# Fused CLI

> Reference for the fused CLI — environment management, file storage, secrets, code execution, and infrastructure commands. Use when writing or explaining shell commands that invoke `fused`, or when helping users set up, switch between, or provision environments. If the commands are part of building or running a project, load `fused-projects` first for the end-to-end model.

- Skill: `fusedio/fused-cli-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add fusedio/fused-cli-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fusedio/fused-cli-2/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: fusedio (https://skillmd.com/u/fusedio)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/fusedio/fused-cli-2

---


# fused CLI reference

> **Part of the Fused skill set — don't work from it alone.** This is the command
> reference. For the workflow that decides *which* commands to run, load
> **`fused-projects`** (project lifecycle) or **`fused-widgets`** (rendering a UI).
> See **`fused-guide`** for the full set.

Check the installed version with `fused --version` (useful for confirming an install before configuring anything).

## Environment selection

Every command targets a specific backend. Two ways to select it:

**Named environment (recommended)** — stored config in `~/.openfused/envs.json`:
```sh
fused --env prod files list
fused --env staging secrets list
```
`--env` can be omitted when the environment is resolved by project manifest or sole-env auto-selection (see resolution rules below).

**Legacy inline selection** — reads config from environment variables:
```sh
fused --backend aws files list        # reads OPENFUSED_* env vars
fused --backend local files list      # host venvs (uv/pip)
fused --backend fused files list      # Fused's managed fused
```

`--env` always wins over `--backend`. `OPENFUSED_ENV` is the env-var form of `--env`.

**Environment resolution order** (first match wins):
1. `--env` flag or `OPENFUSED_ENV` → explicit override (beats everything)
2. Inside a project with `[project].default_env` in `openfused.toml` → manifest pin
3. Exactly one named environment exists → sole-env auto-select
4. Multiple environments, no pin → error naming both fixes: set `default_env` (`fused project set <project> --env <name>`) or pass `--env`
5. No environments → error: run `fused env create`

### Logging

Host logs (`openfused.*` loggers) go to stderr with a timestamp + level + logger-name
format. Set verbosity with `OPENFUSED_LOG_LEVEL` (default `INFO`; accepts `DEBUG`,
`INFO`, `WARNING`, `ERROR`, `CRITICAL`). Use `DEBUG` to surface Lambda cache hits and
cold-start/digest-resolution details when troubleshooting.

```sh
OPENFUSED_LOG_LEVEL=DEBUG fused --env prod code run --file job.py
```

---

## Environment management (`env`)

Named environments bundle all backend config into a named entry in `~/.openfused/envs.json`.

### Create

```sh
# AWS — provisions IAM role + Lambda automatically
fused env create prod --backend aws --prefix myapp- --region us-east-1

# AWS — skip provisioning (config only)
fused env create staging --backend aws --prefix myapp-staging- --no-provision

# Local — bare stdlib venv; scaffolds ~/.openfused/envs/dev/data automatically
fused env create dev --backend local
```

AWS `env create` runs `infra apply` automatically unless `--no-provision` is given. Pass `--no-provision` when the IAM role / Lambda already exist or when you want to review the plan first.

### List and inspect

```sh
fused env list            # all envs with their backend
fused env show prod       # JSON dump of config
fused env show            # config for the resolved environment
```

To pin an environment to a project (the recommended way to avoid per-command `--env`):
```sh
fused project set my-project --env prod   # validates env exists, writes default_env
fused project set my-project --clear-env  # remove the pin
```

### Update fields

```sh
fused env update prod --region us-east-1
fused env update prod --prefix newprefix- --lambda-timeout 600
fused env update prod --audit-bucket my-audit-bucket   # add/change audit bucket
fused env update prod --no-audit-bucket                # remove audit bucket
fused env update prod --require-spec                   # block executions without a spec
fused env update prod --no-require-spec                # remove spec requirement
fused env update prod -p pandas -p duckdb   # set packages (AWS only: baked into the container image)
```

`env update` accepts all the same flags as `env create` (patch semantics — only specified fields change). Use `--no-cache-bucket` / `--no-audit-bucket` to clear a bucket field, and `--no-require-spec` to remove the spec requirement.

### Delete

```sh
fused env delete staging --yes   # removes config only; does NOT teardown AWS resources
```

### Full option reference for `env create`

| Option | Default | Notes |
|---|---|---|
| `--backend` | `aws` | `aws` (Lambda), `local` (host bare venv), or `fused` (Fused cloud). |
| `--region` | `us-west-2` | AWS region |
| `--prefix` | `openfused-` | Lambda function name prefix |
| `--role-arn` | — | Use an existing IAM role instead of creating one |
| `--role-name` | derived | Override the managed IAM role name |
| `--lambda-timeout` | `300` | Execution timeout in seconds |
| `--lambda-memory-mb` | `1024` | Lambda memory (MB). **AWS only.** |
| `--lambda-tmp-storage-mb` | `512` | Lambda `/tmp` ephemeral storage (MB). **AWS only.** |
| `--lambda-architecture` | `x86_64` | Lambda CPU architecture (`x86_64` or `arm64`). **AWS only.** |
| `--lambda-externally-managed` | off | Don't auto-manage the execution Lambda. **AWS only.** Skips the `GetFunction` existence check + `CreateFunction` at execute time (invokes it by name) and skips planning/applying it in `infra plan`/`apply`. Use when the Lambda lifecycle is managed separately (e.g. external IaC). Orthogonal to `--role-arn`, which only short-circuits the IAM role. On `env update`, toggle with `--lambda-externally-managed` / `--no-lambda-externally-managed`. |
| `--docker-image` | — | ECR image URI for the Lambda function. Normally set automatically by `infra build-image`; pass it only to register a pre-built image. |
| `--cache-bucket` | auto-derived | S3 bucket for `input_files` in `execute_code`. Auto-named `<prefix>-cache` by default |
| `--no-cache-bucket` | off | Disable the cache bucket for this env |
| `--audit-bucket` | — | S3 bucket for WORM audit logs. Must have Object Lock enabled; `infra apply` creates it. Also enables verify. |
| `--require-spec` | off | Block `execute_code` calls that omit a `spec`. Also enables verify. Works on all backends. |
| `-p / --package` | — | Pip package to pre-install (repeatable). **AWS only** — baked into the Lambda container image (`image_build.packages`). Errors if passed to a local env. |
| `--system-dep` | — | System package via `dnf` (repeatable). **AWS only** — errors if passed to a local env. |
| `--python-version` | `3.12` | Python version for the container image. **AWS only** — errors if passed to a local env. |
| `--image-platform` | `linux/amd64` | Docker build platform for the container image. **AWS only** — errors if passed to a local env. |
| `--image-repo` | derived from prefix | ECR repository name. **AWS only** — errors if passed to a local env. |
| `--image-tag` | `latest` | Tag for the container image. **AWS only** — errors if passed to a local env. |
| `--builder` | `codebuild` | Image builder. **AWS only** — `codebuild` (default; remote AWS CodeBuild, no local Docker; uses the cache bucket) or `local` (docker build on host). Errors if passed to a local env. |
| `--dockerfile` | — | Path to a user Dockerfile within `--context-dir` (requires `--context-dir`). **AWS only.** |
| `--context-dir` | — | User build-context directory to build instead of the generated Dockerfile. **AWS only.** |
| `--local-path` | `~/.openfused/envs/<name>/data` | Local data directory (**local only**) |
| `--secrets-file` | `~/.openfused/envs/<name>/secrets.json` | Keychain account key identifying the per-env secrets store (**local only**; no file is written) |
| `--no-provision` | off | Skip `infra apply` on AWS |

### Fused (managed-openfused) backend — `--backend fused`

It runs code on **Fused's hosted, managed fused** environment over its data-plane MCP endpoint (an MCP client of the remote fused tool surface). The local side provisions nothing and runs no code itself. Note: `serve` and `infra` commands are not supported on Fused. Create an env with `--backend fused`:

| Option | Default | Purpose |
|---|---|---|
| `--tier` | `prod` | Service tier selecting the base URL (`prod`/`staging`/`unstable`). |
| `--mcp-base-url` | — | Explicit data-plane base URL override (dev/self-host). |
| `--fused-org` / `--fused-env-id` | — | Org + environment (UUID or slug) for the scoped URL; set **together**. Omit both to use the bare endpoint with an env-bound key. |
| `--api-key-secret` | — | Name in fused's local secrets store holding the `ofs_` API key. |

```bash
fused env create fused-prod --backend fused --tier prod \
  --fused-org acme --fused-env-id default --api-key-secret fused/prod-key
```

The API key is resolved (first hit wins) from `--api-key-secret` (local secrets store) → `FUSED_API_KEY` → `FUSED_JWT` (scoped URL only). `serve`/`infra` are not applicable (Fused operates the runtime). Storage is read + presign only (`list_files`/`get_file`); writes and secrets are not exposed by the managed surface yet and raise a clear error.

#### Guided onboarding — the `fused cloud` group

Instead of hand-building the env above, the `fused cloud` group runs the control-plane flow (login → find org/env → wait ready → mint an API key → store it → create the env). Auth0 config defaults to Fused's tenant + the `openfused-server-api` audience; override with `FUSED_CLOUD_AUTH0_DOMAIN` / `FUSED_CLOUD_AUTH0_CLIENT_ID` / `FUSED_CLOUD_AUTH0_AUDIENCE`.

```bash
fused cloud login [--no-browser]            # Auth0 PKCE; caches a control-plane JWT
fused cloud redeem [CODE] [--tier prod]     # redeem a beta invite: admit + create your org + env
                                            #   omit CODE and it prompts (keeps it out of shell history)
fused cloud orgs [--tier prod]              # list your orgs + envs and their provision_state
fused cloud setup [--tier prod] \           # the one-shot guided flow:
  [--beta-code CODE] \                          #   (optional) redeem a beta invite first, then
  [--org O --env E] [--env-name NAME]           #   pick org/env (auto if you have one), wait ready,
                                                #   mint a key, store it, create the `fused` env
fused cloud key create --org O --env E      # mint + store a key for an existing managed env
fused cloud key revoke --org O --id K       # revoke a data-plane key by id
fused cloud logout [--no-browser]           # delete the cached control-plane JWT
fused cloud logout --env NAME               # ALSO delete that env's stored data-plane key (full logout)
```

`setup` stores the minted key in the local secrets store (e.g. `fused/<env-name>-key`) and writes a `FusedCloudEnvironmentConfig` referencing it — never the raw key in `envs.json`. The fused env name defaults to `fused` for the canonical `default` managed env (else `fused-<env>`). The control-plane JWT is flow-scoped; the MCP server never holds it.

Token resolution at request time (first hit wins): **`FUSED_API_KEY`** env var (an explicit override) → the stored **`api_key_secret`** → **`FUSED_JWT`** (scoped-URL only). A configured-but-absent secret falls through rather than failing. **Logging out:** `fused cloud logout` clears the control-plane JWT; add `--env NAME` to also delete that environment's stored data-plane key (then `fused key revoke` to revoke it server-side).

Tune a managed env after creation with `env update <name>` — the managed-fused fields `--tier`, `--mcp-base-url`, `--fused-org`, `--fused-env-id`, and `--api-key-secret` are accepted (mirroring `env create --backend fused`). `infra` commands are not applicable (Fused operates the runtime) and report that posture.

If you have a **beta invite code**, redeem it during the beta gate either as a standalone step (`fused cloud redeem`, after `login`) or folded into setup (`fused cloud setup --beta-code CODE`). Redeeming admits your account and creates a personal org with a `default` environment, which setup then waits on and wires up. The code is single-use; an invalid or already-redeemed code raises a clear error. Prefer the standalone form with the code omitted — it prompts without echo, so the code stays out of `ps` and your shell history; the same applies to `fused cloud accept [TOKEN]`, the sibling command for joining an org you were invited to.

### Verify / security options (all backends)

The `verify` sub-object controls the pre/post-execution security pipeline for `execute_code`. Set fields via JSON patch with `env update`:

```sh
# Enable the verify pipeline with type-checking
fused env update prod --verify '{"enabled": true, "typecheck": true}'

# Also run ty inside Docker so user packages are installed (more accurate)
fused env update prod --verify '{"enabled": true, "typecheck": true, "typecheck_docker": true}'
```

| Field | Default | Notes |
|---|---|---|
| `enabled` | `false` | Master toggle for the full verify pipeline |
| `typecheck` | `false` | Run `ty` type-checking before execution |
| `typecheck_docker` | `false` | When `true` and requirements are set, install packages in Docker first for full import resolution |
| `scan_deps` | `true` | Query OSV for CVEs and typosquatting in requirements |
| `audit_bucket` | — | S3 bucket for WORM audit logs |
| `audit_key_prefix` | `"audit/"` | Key prefix for audit objects |
| `audit_object_lock_days` | — | Retention days for S3 Object Lock (requires bucket with Object Lock enabled) |
| `rules` | — | List of `{"rule_id": str, "severity": "BLOCK"\|"WARN"\|"INFO", "enabled": bool}` overrides |

`typecheck_docker` builds a local mirror image (identified by a hash of base image + requirements, cached across runs) with the user's packages installed, using the env's `docker_image` (or `python:3.12-slim`) as the base. It requires the `docker` CLI on PATH — this is verify tooling only, not an execution backend.

---

## Projects (`project`)

Projects are versioned, deployable collections of UDFs. The on-disk model is: **workspace ⊃ project ⊃ UDF**. All project commands implicitly target the `default` workspace at `~/.openfused/workspaces/default/` (override with `OPENFUSED_WORKSPACES_DIR`).

The first `project new` call auto-creates the default workspace (git init + installs the openfused-managed v5 pre-commit hook). The hook blocks manual commits that touch a UDF's `spec.md` without its entrypoint or vice versa. Use `git commit --no-verify` to bypass it when needed.

### Create a project

```sh
fused project new taxi-pipeline
# Created project 'taxi-pipeline' at ~/.openfused/workspaces/default/taxi-pipeline
```

| Argument | Notes |
|---|---|
| `NAME` | Project name; must match `^[a-z][a-z0-9]*([-_][a-z0-9]+)*$`, max 64 chars |

### List projects

```sh
fused project list
```

Prints all project names in the default workspace, sorted. Prints a help message when none exist yet.

### Add dependencies to a project

```sh
fused project add-dep taxi-pipeline duckdb pandas        # runtime deps
fused project add-dep taxi-pipeline pytest coverage --dev # dev deps (for `code test`)
```

Runs `uv add [--dev] <packages>` then `uv sync` inside the project's `scripts/`
dir in one step, so the lockfile and the installed venv stay in step — avoiding
the stale-venv warning (and silent cache-disable) a bare `uv add` would leave
behind. Never hard-fails on tooling problems (missing `uv`, `OPENFUSED_LOCAL_INSTALLER=pip`,
non-zero exit) — it prints a guided `Warning:` instead. Errors only on an unknown project.

### Show a project

```sh
fused project show taxi-pipeline
```

Re-syncs the `openfused.toml` manifest from disk first (structured merge: discovers UDF folders under `scripts/`, rewrites inventory, preserves user-set fields like `description`/`auth`/`cache_max_age` and TOML comments), then prints JSON with keys `name`, `path`, and `udfs`. Exits with an error when the project does not exist.

### Delete a project

```sh
fused project delete taxi-pipeline
```

Removes a project from the default workspace: `git rm -rf -- <name>` followed by
a `--no-verify` commit, then cleans gitignored/untracked residue (`scripts/.venv`,
`__pycache__`). Rejects `_core` and any underscore-prefixed (reserved) name with a
`ValueError`. Prints JSON `{name, deleted, root}` on success.

### Naming rules

Project and UDF names are **lowercase slugs**: `^[a-z][a-z0-9]*([-_][a-z0-9]+)*$`, max 64 chars. Both `-` and `_` are accepted as segment separators, so snake_case UDF names like `list_comments` are valid. Lowercase-only prevents case-collision bugs on case-insensitive filesystems and in S3 key segments.

### Workspace layout

```
~/.openfused/workspaces/default/    # the default workspace (one git repo)
├── .git/                           # openfused-managed pre-commit hook installed here
├── taxi-pipeline/                  # a project = one folder
│   ├── openfused.toml              # manifest (synced by the MCP project_show tool / at deploy)
│   ├── SKILL.md                    # project contract (agents read this for context)
│   ├── assets/                     # static project assets
│   ├── references/                 # dataset notes + findings (one file per topic)
│   ├── widgets/                    # saved project dashboards
│   └── scripts/
│       ├── pyproject.toml          # uv-managed Python deps (note: uv's [project] table, not fused's)
│       ├── tests/                  # project-level pytest suites
│       ├── taxi-analysis/          # a UDF, kind: py
│       │   ├── main.py
│       │   ├── spec.md
│       │   └── test_main.py
│       └── trip-dashboard/         # a UDF, kind: json
│           ├── main.json
│           └── spec.md
└── sales-app/
```

UDF kind is inferred from the entrypoint: `main.py` → `py`, `main.json` → `json`. A folder with both is an error (ambiguous kind). Dot-prefixed and underscore-prefixed directories are skipped.

### Authoring UDFs (agent-authored)

There is no `udf generate` or `project regenerate` command. UDFs are authored by the driving agent:

1. Write `scripts/<name>/spec.md` and get it approved.
2. Write the entrypoint — `scripts/<name>/main.py` for a `py` UDF, or `scripts/<name>/main.json` for a `json` widget UDF.
3. Validate with `fused code verify <file>` (CLI) or MCP `verify_code` before committing.
4. Commit `spec.md` + entrypoint together — the pre-commit hook enforces that spec and entrypoint are always paired in the same commit.

See the **fused-projects** skill for the full spec-first, agent-authored flow (env → project → UDF → run → widget → deploy).

### Deploy a project (`project deploy`)

Batch-deploys all UDFs in a project to a channel. The workspace must be clean (all changes committed) unless `--force` is used.

```sh
fused project deploy taxi-pipeline                     # deploy all UDFs to preview
fused project deploy taxi-pipeline --channel release   # deploy all UDFs to release
fused project deploy taxi-pipeline --force             # bypass dirty-tree check
```

| Option | Default | Notes |
|---|---|---|
| `NAME` | required | Project name |
| `--channel` | `preview` | `preview` or `release` |
| `--force` | false | Deploy even with uncommitted changes |

> `--channel release` bypasses the preview gate and breaks the rollback
> invariant (rollback targets must be prior release events). Use it only for
> bootstrapping the very first release URL — never for a routine production
> release, which goes `deploy` (preview) → `promote`. See the `fused-projects`
> guardrails.

Requires AWS env + `cache_bucket` + provisioned serving plane (`fused infra serve`). Echoes the resolved env name; prints one URL per UDF. Exits 1 if any UDF fails to deploy.

### Promote a project (`project promote`)

Batch-promotes all UDFs in a project from preview to release.

```sh
fused project promote taxi-pipeline
```

### Show project deploy status (`project status`)

Shows the live cloud deploy snapshot for a project. Marks UDFs that are in the cloud snapshot but absent on disk as **orphaned** (prompt to restore or retire).

```sh
fused project status taxi-pipeline
```

Output columns: `UDF`, `CHANNEL`, `COMMIT`, `ORPHANED`, `URL`.

### Deploy a single UDF (`udf deploy`)

```sh
fused udf deploy analysis --project taxi-pipeline
fused udf deploy analysis --project taxi-pipeline --channel release
fused udf deploy analysis --project taxi-pipeline --force
```

| Option | Default | Notes |
|---|---|---|
| `NAME` | required | UDF name |
| `--project WF` | required | Project that owns the UDF |
| `--channel` | `preview` | `preview` or `release` |
| `--force` | false | Deploy even with uncommitted changes |

Requires a clean git tree (or `--force`), AWS env + `cache_bucket`, and a provisioned serving plane. Echoes the resolved env on stderr and prints the channel URL on stdout.

### Promote a single UDF (`udf promote`)

Repoints the release channel to whatever commit preview is currently running.

```sh
fused udf promote analysis --project taxi-pipeline
```

| Option | Default | Notes |
|---|---|---|
| `NAME` | required | UDF name |
| `--project WF` | required | Project that owns the UDF |

### Roll back a single UDF (`udf rollback`)

Rolls back the release channel to a prior commit. Defaults to the previous release commit when `--to` is omitted.

```sh
fused udf rollback analysis --project taxi-pipeline
fused udf rollback analysis --project taxi-pipeline --to abc123def
```

| Option | Default | Notes |
|---|---|---|
| `NAME` | required | UDF name |
| `--project WF` | required | Project that owns the UDF |
| `--to COMMIT` | previous release | Target commit SHA |

### Retire a UDF (`udf retire`)

Revokes the UDF's preview + release mounts, appends a retire event, and drops the UDF from the deploy snapshot. **This cannot be undone via this command.** Prompts for confirmation unless `--yes` is passed.

Retire enforces the same workspace-id conflict gate as deploy/promote/rollback: if the live snapshot was written by a different workspace it refuses unless `--force` is passed (an intentional takeover).

```sh
fused udf retire analysis --project taxi-pipeline
fused udf retire analysis --project taxi-pipeline --yes
fused udf retire analysis --project taxi-pipeline --yes --force   # take over a foreign-owned UDF
```

| Option | Default | Notes |
|---|---|---|
| `NAME` | required | UDF name |
| `--project WF` | required | Project that owns the UDF |
| `--yes` | false | Skip the confirmation prompt |
| `--force` | false | Take over a UDF deployed from a different workspace |

### Pre-commit hook (v5)

The workspace `.git/hooks/pre-commit` is installed/upgraded by `project new` (and by any `bootstrap_workspace` call). Current version: `v5`.

The hook blocks manual commits that touch a UDF's `spec.md` without its entrypoint (`main.py`/`main.json`), or vice versa — including one-sided deletions. The pairing is enforced at depth 4: `<project>/scripts/<udf>/<file>`. Tests, resource files, and other files commit freely. fused's own auto-commits are always paired and pass through without `--no-verify`.

Use `git commit --no-verify` to bypass the hook when needed (e.g. fixing a typo in spec.md alone), but note this bypasses the spec↔entrypoint pairing check.

The hook body embeds `# openfused-managed pre-commit hook v5`. On re-install, older managed versions are upgraded; unmanaged hooks (no marker) are warned about and never overwritten.

---

## File storage (`files`)

### List

```sh
fused files list                        # list all buckets
fused files list --bucket my-bucket     # list all keys in bucket
fused files list --bucket my-bucket --prefix data/2024/
```

### Count

```sh
fused files count --bucket my-bucket
fused files count --bucket my-bucket --prefix logs/ --ext .parquet --ext .csv
```

### Get presigned URL

```sh
fused files get --bucket my-bucket --key data/report.parquet
fused files get --bucket my-bucket --key data/report.parquet --expires-in 7200
```

### Schema inspection

Prints column names, types, row count, and file metadata for Parquet, Arrow IPC, or CSV files.

```sh
fused files schema --bucket my-bucket --key data/report.parquet
```

### Upload

```sh
fused files upload data.parquet --bucket my-bucket --key uploads/data.parquet
cat data.csv | fused files upload - --bucket my-bucket --key uploads/data.csv
```

`SRC` defaults to stdin when omitted; `-` also reads from stdin.

---

## The web UI (flow) — separate tool, out of scope

The local web UI (project pages, task → agent runs with live transcripts, the
widget board) is **flow** — a **separate client** (`fusedio/flow`), started with
the `flow` CLI (or `npx @fusedio/flow` once published). It is **not** a `fused`
subcommand and is **out of scope** for this reference. flow talks to this backend
over `fused dev serve` and the `_core` UDFs. The `fused` CLI's own human-facing
widget surfaces are `fused widget open` / the parley on the standalone
**widget-host** (below) — no full UI needed.

---

## Health check (`doctor`)

`fused doctor` surveys **every** workspace under `~/.openfused/workspaces/`
plus the built-in `_core` workspace and reports per-project health findings. It
exists to turn latent layout/venv drift — the kind that makes a project's widgets
render empty with a confusing `` `.venv` not found `` error only at use time — into
one up-front, actionable report.

```sh
fused doctor          # read-only survey (default)
fused doctor --fix    # remediate the fixable findings, then re-diagnose
```

**Read-only by default** — diagnosis runs the existing resolvers as pure probes
(never `uv sync`, never rewrites a manifest, never re-materializes `_core`). It
prints one block per scope (`_core` first, then `<workspace>/<project>`); a scope
with no issues prints `OK`.

Findings carry a severity — `BLOCK` (broken / won't run) or `WARN` (degraded):

| `rule_id` | Severity | Meaning | Fixable by `--fix`? |
|---|---|---|---|
| `invalid-name` | BLOCK | Project dir name is not a valid slug | no (rename manually) |
| `legacy-layout` | BLOCK | Legacy v1 UDF folder (`main.py`/`main.json`) at project root, no `scripts/` dir | yes (migrate) |
| `venv-missing` | BLOCK | `scripts/.venv` absent/incomplete | yes (`uv sync`) |
| `venv-stale` | WARN | venv older than `pyproject.toml`/`uv.lock` | yes (rebuild) |
| `stray-root-venv` | WARN | Stale root `.venv` beside a valid `scripts/.venv` | yes (remove) |
| `manifest-legacy` | WARN | `openfused.toml` uses `[workflow]` not `[project]` | yes (migrate) |
| `manifest-unreadable` | BLOCK | `openfused.toml` missing/unparseable | no (repair manually) |
| `env-unresolved` | BLOCK | Project env doesn't resolve | no (create/pin an env) |
| `core-uv-missing` | BLOCK | `uv` not on PATH | no (install uv) |
| `core-cache-broken` | BLOCK | A `_core` project lacks its built venv | yes (re-materialize) |
| `core-stale` | WARN | `_core` cache stamp ≠ installed wheel | yes (re-materialize) |

**`--fix`** applies the fixable findings — migrate first (so `scripts/` exists),
then build/refresh the venv, remove stray root venvs, and re-materialize `_core` —
then re-diagnoses and prints the residual. `invalid-name`, `env-unresolved`, and
`manifest-unreadable` always need a human and are never auto-fixed.

**Exit code:** `doctor` exits `1` when any `BLOCK` finding remains (after
remediation, under `--fix`); `WARN`-only or clean exits `0` — so it works as a CI
gate.

---

## Workspace projects (`project`)

A **project** is a directory rooted at an `openfused.toml` manifest — the unit of
scope and memory. Projects are discovered by directory listing
under `~/.openfused/workspaces/default/` (no registry file). Resolution is
git-style: explicit name → `OPENFUSED_PROJECT` → manifest walk-up from cwd →
global scope (pre-project behavior unchanged).

```sh
fused project create taxi --description "NYC taxi analysis"   # scaffold under workspaces/default/taxi
fused project create taxi --env prod-aws                      # scaffold and pin default_env (validates env exists)
fused project list                       # registered projects (name + path + exists flags)
fused project show [NAME]                # the context packet (same as get_project_context)
fused project set NAME --description "new words" --env prod   # update the manifest's [project] keys
fused project set NAME --clear-env       # remove default_env from the manifest
fused project serve --mcp --project NAME # serve ONE project as a READ-ONLY stdio MCP (the product tier)
```

> **`project use` was removed (ITEM-11816).** Select a project with one of:
> - `export OPENFUSED_PROJECT=NAME` — persists across commands in the shell
> - `cd` into the project directory — cwd walk-up resolves automatically
> - `--project NAME` per individual tool/CLI call

`set` updates the `[project]` table of a registered project's `openfused.toml`
(at least one of `--description` / `--env` / `--clear-env` required; `--env`
and `--clear-env` are mutually exclusive). Edits are style-preserving —
comments, formatting, and unknown keys/tables in the manifest survive — and it
prints the updated `{name, description, default_env}` as JSON.

`create` scaffolds the standard layout: `openfused.toml`, a `SKILL.md` contract,
and the `scripts/`, `widgets/`, `references/`, `assets/` convention directories
(each with a README stating its purpose; `scripts/` also contains `pyproject.toml`
and `tests/`). Each UDF lives as a folder `scripts/<name>/` with `main.py` (or
`main.json`) as the entrypoint and `spec.md` as its contract — UDFs anywhere else
are not listed or served. A project scopes:

- **environment** — the manifest's `default_env` is used when no `--env` /
  `OPENFUSED_ENV` override is given (explicit override always wins); with a single
  named environment and no project pin, the sole env is auto-selected;
- **widget workspace** — `widget open/push/watch/parley` default to the
  project's `widgets/` directory (explicit `--dir` wins);
- **audit** — every event is stamped with the project name; filter with
  `audit log --project NAME`.

Start agent work in a project with `fused project show` (or the
`get_project_context` MCP tool) — one call returns identity, the SKILL.md
contract, reference notes, widgets, UDF scripts, and the resolved environment.

### Serve a project as a read-only MCP (`project serve --mcp`)

`fused project serve --mcp [--project NAME]` publishes **one** project as the
**read-only external MCP product tier** — a queryable data
source any MCP client (Claude Desktop, a BI tool, another agent stack) can connect
to. The closed surface is:

- **`get_project_context`** — the orientation packet (a pure read);
- **one tool per published UDF** in `scripts/` (named by its stable node `udfName` —
  the file stem; params from the `@fused.udf` signature; calling it runs the UDF as
  a *cached query* against the project's `default_env` — never arbitrary code). When
  the project has a readable pipeline graph (`canvas.toml`, or the implicit floor
  derived from `{{ref}}`/`fused.load` scans), each UDF tool's description is enriched
  read-only with **upstream/downstream lineage** (`reads: …` / `feeds: …`) and the
  `{{ref}}` **argument names** that drive it. A missing/corrupt
  `canvas.toml` simply omits the annotation — it never breaks tool publishing;
- **`widget://` / `reference://` resources** — each saved widget (config + resolved
  data) and reference note, readable point-in-time.

```sh
fused project serve --mcp --project taxi-analysis   # read-only stdio MCP for one project
fused code serve ./taxi --mcp                       # equivalent: serve the project DIR read-only
```

`--mcp` is **required** (the only serve mode in Phase 2 — stdio-only). `--project`
is **authoritative** (wins the resolution chain); the process `cwd` is set to the
project root so the data plane resolves `default_env` and the convention
directories. `code serve <project-dir> --mcp` is the equivalent path (it serves the
project at that directory).

**It never registers a write tool.** This is the one hard difference from the bare
`fused` stdio server (run with no subcommand), which registers the full *gated*
RW surface (`upload_file`, `put_secret`, `cache_clear`, and the
`--enable-infra`/`--enable-destructive` tools). The read-only path is a **separate
registry** that never *constructs* those tools — they are unreachable, not merely
gated. It is the binary the app's "Serve as MCP" connect snippet bakes:
`<openfused-bin> project serve --mcp --project <name>`.

---

## Pipeline graph (`pipeline`)

The project's UDFs wired into a persisted, versioned graph — nodes, `{{ref}}`
edges, and a `canvas.toml` home. Both commands resolve the
project the same way as `project show` (explicit `--project` → `OPENFUSED_PROJECT`
→ `openfused.toml` walk-up) and emit the `Pipeline` JSON
(`{name, path, version, nodes, edges, viewport}`) to stdout. They are the seam the
flow UI reads/writes the canvas through; the graph is a **design-time
lens** and never enters the resolve loop.

```sh
fused pipeline graph                        # read the derived/persisted graph as JSON
fused pipeline graph --project taxi          # explicit project
fused pipeline graph --canvas pipelines/reporting/canvas.toml  # a named canvas
fused pipeline derive                        # "create canvas": write canvas.toml + emit reloaded graph
fused pipeline derive --project taxi          # explicit project
```

- **`graph`** reads the graph: it unions the **implicit** edges (widget→UDF from
  the shared `{{ref}}` SQL scanner, UDF→UDF from a static `fused.load(...)` scan)
  with any **explicit** `[canvas].edges` authored in `canvas.toml`. A **missing**
  `canvas.toml` still yields the derivable graph (the implicit-scan floor, `version`
  null); a **corrupt** `canvas.toml` surfaces as a CLI error (non-zero exit), never
  a crash.
- **`derive`** is the derive-and-persist write path: it runs
  the implicit scan, lays nodes out left-to-right by stage depth, and writes a
  `canvas.toml` at the project root (or `--canvas PATH`) capturing the derived nodes
  + edges as **explicit lineage** (whole-document atomic write). It then emits the
  **reloaded** graph, so `version` is the `sha256:<hex>` content hash of the file
  just written. The Python core is the only `canvas.toml` writer (the mutation
  boundary).

---

## Audit log (`audit`)

```sh
fused audit log                          # last 50 events
fused audit log --limit 100
fused audit log --status blocked         # blocked executions only
fused audit log --event-type execute_code --status warned
fused audit log --project taxi           # events recorded under one project
```

Events are read from the local SQLite audit store (`~/.openfused/audit.db`) — the same database the `get_audit_log` MCP tool reads, so they persist across server restarts. For durable cross-session/cross-instance history, configure an `audit_bucket` in the environment; the `get_audit_log` MCP tool then merges S3-stored events when given a date range.

---

## Secrets (`secrets`)

```sh
fused secrets put db-password                # create or update (prompts for the value)
fused secrets get db-password                # print value
fused secrets list                           # all secrets
fused secrets list --prefix db-             # filter by prefix
fused secrets delete db-password             # delete (prompts; --yes to skip)
```

`secrets delete` errors on a missing secret (`Secret '<name>' not found`). On
AWS the secret is **scheduled** for deletion with the default 30-day recovery
window, not force-deleted; on the local backend the name is removed from the
OS keychain map immediately. The MCP equivalent (`delete_secret`) requires the
server to run with `--enable-destructive`.

**Keep the value off the command line.** `put` takes the value from a no-echo
prompt when you omit it, so the secret never lands in argv — where any other
user on the host can read it (`ps`, `/proc/<pid>/cmdline`) — or in your shell
history. For scripts and CI, pipe it or point at a file:

```sh
printf %s "$DB_PASSWORD" | fused secrets put db-password   # piped stdin
fused secrets put db-password --value-file ./db-password   # from a file ('-' = stdin)
fused secrets put db-password "s3cr3t"                     # inline: works, but exposed
```

One trailing newline is stripped from piped/file input, and an empty read is an
error rather than an empty secret.

**Naming requirement for Lambda access**: the Lambda execution role can only read secrets whose name starts with the environment's function prefix (e.g. `openfused-`). Always prefix secret names with the function prefix when they need to be read from `execute_code`:

```sh
fused secrets put openfused-db-password   # readable from Lambda
fused secrets put db-password             # NOT readable from Lambda
```

Never pass secret values through `code run` inline code strings — retrieve them inside the execution using `openfused.get_secret("openfused-...")` (works on AWS and the local backend).

---

## Code execution (`code run`)

Runs Python code on the active backend (Lambda for AWS, a host-venv subprocess for local). Assign `result` to return a value.

```sh
# Inline code — requires -c/--code
fused code run -c "result = 1 + 1"

# From a file (auto-detected; --file flag is optional)
fused code run myanalysis.py

# From stdin
cat myanalysis.py | fused code run

# Pass local files into the execution context
fused code run myanalysis.py --input-file data.parquet --input-file config.json
```

pip requirements are configured per-environment via `env update -p`, not per-call. Set them once:

```sh
fused env update prod -p pandas -p duckdb
```

Output format:
- `stdout` is printed as-is
- `stderr` goes to stderr
- `result: <value>` is printed when `result` is set

**Keep the package set stable across calls.** On AWS, packages are baked into the container image — changing them means rerunning `fused infra build-image` (build + ECR push). On local, venvs are cached by a hash of the package set — changing it rebuilds the venv (seconds with uv).

**Caching (`cache_max_age` / `cache_refresh`) is not available via `code run`** — it is only exposed through the MCP `execute_code` tool. Use the MCP tool when you need result memoization.

`--monitor-interval` (CloudWatch poll seconds during a run) defaults to **10** on `code run`; the MCP `execute_code` tool defaults to **30**.

**Local backend — project venv (`--project` or `--project-dir`).** On a local environment, pass one of:

- `--project <name>` — workspace-registered project; venv must already exist (`uv sync`).
- `--project-dir <path>` — ad-hoc path to any directory containing `openfused.toml`; venv is materialised in place on first run via `uv sync` in `<dir>/scripts/`. Use this for skill-folder bundles (e.g. `~/.claude/skills/<project>`) without registering them in the workspace.

The two flags are **mutually exclusive**. Both are **local-only** — rejected with a clear error on AWS and Fused backends. Without either flag, local execution uses a bare stdlib-only venv (third-party imports fail).

```sh
# Workspace-registered project (venv must already exist)
fused code run myanalysis.py --project taxi-pipeline

# Ad-hoc path (venv materialised on first run via uv sync)
fused code run myanalysis.py --project-dir ~/.claude/skills/taxi-pipeline
```

---

## Security scan without execution (`code verify`)

Scans code and input files for security issues without running it. Packages configured in the resolved environment are scanned for CVEs. Exits 1 if any BLOCK-severity finding is produced.

```sh
# Scan a file
fused code verify myanalysis.py

# Inline scan
fused code verify -c "import subprocess; result = 1"

# Scan code + input files for PII and path traversal
fused code verify myanalysis.py --input-file data.csv

# Spec check — Claude reviews whether code matches description (requires an Anthropic
# API key: ANTHROPIC_API_KEY env var, or `fused secrets put anthropic-api-key`, which
# prompts for the key)
fused code verify myanalysis.py --spec "compute the mean of column A"

# Scan using a workspace project's deps
fused code verify myanalysis.py --project taxi-pipeline

# Scan using an ad-hoc project dir's deps (local-only; no backend execute)
fused code verify myanalysis.py

…(truncated)
