CLI reference
fabric is a non-interactive client: you pass everything as flags and it
never stops to prompt you. By default it prints human-readable output; add
--json to any command to get the same result as structured JSON for scripts
and agents to parse. fabric --help and fabric <cmd> --help are always
accurate: consult them rather than guessing, since the live command set is the
source of truth.
Auth
| Command | What it does |
|---|---|
fabric signup | Browser sign-up + CLI auth in one step. |
fabric login | Device-auth in the browser; saves your credentials. |
fabric login --no-browser | Headless device-code login: prints an activation URL + one-time code to approve from any browser. |
fabric whoami | Show the signed-in account. |
Agent principals
Agent principals are named, org-scoped identities for automated callers (CI bots, coding agents, deploy pipelines). Each agent gets its own API key, appears as a distinct participant in channels, and can be revoked independently.
| Command | What it does |
|---|---|
fabric agent create "<name>" | Register a new agent and mint its API key. The key is printed once; capture it. The calling identity is recorded as the sponsor. |
fabric agent list | List the org’s agents (id, name, status). |
fabric agent revoke <id-or-name> | Revoke an agent and invalidate its key immediately. Accepts a UUID or the agent’s name. |
fabric agent serve | Run the local agent bridge daemon. Authenticates with an agent API key, follows the hub’s event stream for @-mentions, opens a session per thread, and posts replies via the channel API. |
Use the minted key as FABRIC_TOKEN or pass it via --token when running the
agent. The agent authenticates on every /v1 request exactly like a user key,
but its identity resolves to the agent principal, so channel messages and audit
entries attribute the work to the agent (with its sponsor visible).
fabric agent serve flags
| Flag | Default | Description |
|---|---|---|
--agent-key | (env FABRIC_AGENT_KEY) | The agent’s API key. Required. |
--idle-timeout | 30m | Close idle sessions after this duration. A new mention on the same thread reopens the session. |
--adapter-cmd | npx | ACP adapter binary or command. Override to use a different ACP-compatible adapter. |
--adapter-args | @zed-industries/claude-code-acp | Arguments for the adapter command. |
--stub | off | Use the built-in echo responder instead of the ACP adapter (for testing). |
--json | off | Structured JSON log output on stdout. |
--hub | saved login | Hub URL (same resolution as other commands). |
Data directory. The bridge persists its event cursor and session state under
~/.config/fabric/agents/<agent-name>/state.json (respects FABRIC_CONFIG_DIR).
On restart it resumes from the persisted cursor, so no events are reprocessed.
Agents can also be managed from the settings UI under Team > Agents.
The change loop
| Command | What it does |
|---|---|
fabric change create "<name>" | Fork a new draft change off the current trunk frontier. Becomes the most recent change. --description "<markdown>" stores an intent statement reviewers see rendered on the change page. --ticket TIX-N links the change to the ticket it implements (the key must exist); the change page then links straight to it. |
fabric clone | Materialize the most recent change into a working directory (the repo name by default). The target must be empty: if it already holds files, clone refuses and writes nothing, so it never clobbers work on disk. |
fabric clone <dir> | Clone into <dir> (same as --dir <dir>). Use fabric clone --dir . to populate the current directory. A leftover .fabric/ from a prior clone does not count as content, so re-cloning an otherwise-empty dir works. |
fabric clone --force | Write the checkout even if the target is not empty. Files with the same path are overwritten; unrelated files are left in place. |
fabric pull | Re-materialize the tracked change’s current tip into the working dir (pick up ops others pushed). If the change is no longer live (accepted, abandoned, or deleted), pull explains which and points you at the remedy instead of failing with a bare error. |
fabric status | Local diff of your working tree vs the cloned base. No network. |
fabric diff | Local detailed diff. No network. |
fabric push | Upload your working tree to the change as ops. Idempotent. Does not touch trunk or merge anything. If the change is already proposed, a push re-runs its hub-side checks against the updated diff. |
fabric propose | Move the change draft → proposed (open it for review). Rejected if the change’s fabric.yaml is malformed. -m "<markdown>" (alias --description) sets or replaces the change’s description in the same step; without it the existing description is kept. |
fabric change describe "<markdown>" | Replace the tracked change’s description: the intent statement reviewers see rendered above the diff. Pass an empty string to clear it. |
fabric change ticket <TIX-N> | Link the tracked change to the ticket it implements, so its page points straight at the ticket (the structured record of intent) instead of relying on a TIX-N: name prefix. The key must resolve to an existing ticket. Pass an empty string to clear the link. |
fabric change link add <url> | Attach a labeled URL to the tracked change, shown on its page for reviewers. The typical use is a workflow or agent posting its preview deployment URL. Only http/https URLs are accepted. --label "<text>" sets the display text (defaults to the URL’s host). Re-adding the same label + url returns the existing link instead of duplicating it. |
fabric change link list | List the tracked change’s links: id, label, and url, oldest first. |
fabric change link remove <id> | Remove one link from the tracked change by its id (from link list). |
fabric approve | Record an approving review on the change. --request-changes records the opposite; --note "<text>" attaches a message. |
fabric accept | Land the change into trunk through the gated pipeline. Blocked until the repo’s approval policy (set in fabric.yaml) is met. The hub merges the change onto the current trunk, runs the required checks against that exact merged result, and advances trunk only if they pass. If trunk moves while the checks run, the hub re-merges on the new tip and re-checks, so trunk only ever lands on a state a check passed against. When the trunk batches accepts (see the merge queue below), accept does not land inline: it returns queued and the change lands in the background once it passes its checks. |
fabric abandon | Discard the change. |
The merge queue
When a trunk batches accepts (batch_size or concurrency_window above one in
queue.toml), an accepted change does not land inline. It merges into a
staged candidate frontier (trunk plus the queued changes), is tested there, and advances trunk
only once a check passes against it. Several changes can be trialled at once, stacked in order, and
the longest passing prefix lands together. A change that fails is evicted and the ones behind it
re-form without it. Trunk only ever lands on a state a check passed against.
| Command | What it does |
|---|---|
fabric queue | Show the queue: whether the trunk batches accepts, the resolved window and batch size, the staged cut (base frontier and the candidate frontier it projects to), the ordered positions of the changes waiting to land, each candidate’s status, and the recent terminal outcomes. --json for machine-readable output. A pure read. |
On a repo that never opts into batching (the zero-config default, batch size 1), the queue is
always empty: each accept lands inline once its checks pass, and fabric queue says so.
Checks and runs
| Command | What it does |
|---|---|
fabric check | Run the change’s .github/workflows/*.yml locally in Docker and print per-job pass/fail. Fully local: nothing is sent to the hub, so it works offline. Needs Docker running. |
fabric check --report | Same local run, but afterward record the result on the hub so it shows up in the runs list, tagged local. Best-effort: if the hub is unreachable the local result still stands and the command does not fail. |
fabric checks | Show the hub-side check runs for the change (the ones that run when you propose, push to an already-proposed change, or accept). By default it polls until they finish, re-rendering the per-job table as jobs change so you see progress, not just a final dump. A job that fails because a tool was missing from the CI runner (e.g. a workflow shells out to make and the runner has none) is shown as SETUP ERROR rather than FAILURE, so an environment gap is not mistaken for a problem with your change. |
fabric checks --no-wait | Print the current check states once and exit, without polling. --once is an alias. With --json it returns the single snapshot. |
fabric checks --logs <job> | Download one job’s full log instead of polling, so a failed check is debuggable from the terminal without opening the web runs page. Name the job by its id (e.g. lint), its workflow / job label if two workflows share a job name, or its check-run id. |
fabric checks --timeout <d> | How long to keep polling with no progress before giving up (default 30m). Any job status change resets the clock, so a run that is still advancing is never abandoned. Pass 0 to wait indefinitely. |
Checks only run once a change is proposed, so fabric checks on a draft
change has nothing to poll: it tells you to run fabric propose first instead of
blocking. If the checks make no progress for the --timeout budget, the command
prints the last states it saw and exits non-zero, so you always get status even
on a stuck run.
A run reported with --report is marked local in the web runs list so
reviewers can tell it ran on your machine rather than on hub infrastructure. A
local run is informational only: it never counts toward the required-checks gate
that fabric accept enforces.
When a check fails, fabric checks names the failing step in the summary
(for example Step 'Run linter' failed), and each check in the --json payload
carries a web_url (the job’s block on the web runs page) and a log_url (the
REST endpoint fabric checks --logs reads). Reach for --logs <job> to pull the
full output and diagnose the failure in place.
The hub runner has no Docker daemon, so a workflow that needs one (a services:
container, a container: job, or a Docker-based action) fails with a clear
message instead of running part way. See
Runner compatibility for the supported subset.
Build artifacts
A workflow step that runs actions/upload-artifact
uploads named files during a run, exactly as on GitHub. Fabric captures each
uploaded artifact and stores it against the run, so you can download it afterward.
Artifacts show up in two places:
- The run-detail page on the web runs view lists every artifact the run
produced, each with its size and a Download link. The bytes stream back
through the hub as a
.zip. - The API, for agents and scripts.
GET /v1/vcs/runs/<run-id>/artifactsreturns the run’s artifacts as JSON (id,name,size,download_url), andGET /v1/vcs/artifacts/<id>/downloadstreams one artifact’s zip. Therun-idis the run-group (batch) id you see on the runs page; both endpoints are scoped to your org, so you can only ever fetch an artifact your org owns.
Uploading uses the standard action, so no Fabric-specific workflow change is needed:
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
Artifacts are retained alongside the run. When old runs age out of a change’s retained history, their artifacts are reclaimed with them.
Comments
Comments live on the hub, not in your clone, so these verbs are the only way to read or
answer them. Read comment ids from fabric comments list --json, then pass them to the
write verbs.
| Command | What it does |
|---|---|
fabric comments list | List the comments on the files this clone pulled. Each is resolved against your clone’s base frontier, so its line number points at your working copy. |
fabric comments list --path <p> | Only list comments on one file. |
fabric comments list --change <id> | List a change’s review thread instead of the clone’s files (no clone needed). |
fabric comments add --path <p> --lines A-B "<body>" | Leave a comment on a line span (or a single line, --lines A). |
fabric comments reply <id> "<body>" | Reply to a comment thread. Nested under the thread root. |
fabric comments resolve <id> | Mark a comment resolved. |
fabric comments unresolve <id> | Reopen a resolved comment. |
fabric comments react <id> --emoji 👍 | Toggle an emoji reaction (--off removes). Palette: 👍 👎 😄 🎉 ❤️ 🚀 👀. |
list --json returns each comment’s id, anchor, body, author, thread parent_id,
resolved state, outdated flag, and reactions. These are the same comments the web file
viewer shows. The hub resolves each anchor for you: a comment whose target moved or vanished
since it was written is marked (outdated), and a text comment that can no longer be placed
shows as unplaced with its original quoted text. Comments on non-text files carry their
own coordinate instead of a line: a PDF page (--page N), an audio/video moment
(--time-ms N, with an optional --time-end-ms), or an image box
(--region "[x,y,w,h]").
Convention: resolve a comment only after the push that addresses it has landed, and reply
first with what changed, so the fix is traceable from the thread.
Tickets
Tickets are version-controlled markdown that live in your repo: each one is a file with a
front-matter header (title, status, assignee, priority, labels) and a markdown body. Creating
or editing a ticket records ops in the operation log, so a ticket carries its full history like
any other file. The hub keeps a fast read model behind list and show.
| Command | What it does |
|---|---|
fabric ticket list | List tickets, highest priority then most recently updated first. |
fabric ticket list --status <s> | Only list tickets with that status. |
fabric ticket list --assignee <who> | Only list tickets with that assignee. |
fabric ticket show <key> | Show one ticket by key (e.g. TIX-12), including its body. |
fabric ticket create "<title>" | Create a ticket. The hub mints its key (TIX-N). |
fabric ticket edit <key> --status done | Edit a ticket. Only the flags you pass change. |
fabric ticket edit <key> --blocks TIX-2,TIX-5 | Set the typed relations (see below). |
create accepts --status (defaults to your workflow’s first state), --assignee, --priority
(higher sorts first), --labels (comma-separated), and --body (markdown). edit takes the same
flags plus --title; only the flags you pass are changed, so fabric ticket edit TIX-12 --status done leaves everything else as-is. Pass --labels "" to clear labels.
Relations (TIX keys in the same workspace) are set with --blocks, --blocked-by, and
--related (each a comma-separated list) and --parent (a single key, making the ticket a
sub-issue). On edit each relation flag REPLACES that whole relation with what you pass, so
--blocks TIX-2,TIX-5 sets exactly those two and --related "" clears it. An unknown, cross-org,
or self target is rejected. show prints every non-empty relation plus the sub-issues and the
tickets that reference this one. Read keys from fabric ticket list --json, then feed them to
show and edit.
Reporting Fabric bugs
fabric ticket files into your own tracker. When the problem is with Fabric itself (a broken
runner, a CLI that misbehaves, an engine error, a missing feature), fabric report sends it
upstream to the Fabric team instead, from the terminal where it happened.
| Command | What it does |
|---|---|
fabric report "<title>" | File a report with the Fabric team. Attaches an auto-captured context envelope (see below) and returns the report id. |
fabric report "<title>" --category <c> | Set the triage lane: runner, cli, engine, or feature-request (default cli). |
fabric report "<title>" --body "<text>" | Add a body with repro steps or expected-vs-actual. |
fabric report "<title>" --attach-logs | Also attach the tracked change’s last failed check run and its log (opt-in). |
fabric report --from-last-error | Build the report from the last fabric command that failed, deriving the title and body from it. |
fabric report status <id> | Poll a filed report’s status so you see the Fabric team’s response without leaving the terminal. |
Every report carries a context envelope the CLI gathers for you, so a report is reproducible by
construction rather than whatever you remember to paste: the fabric version, the hub URL, the
repo id, the tracked change id, and your OS/arch. Pass --change <id> to set the change
explicitly when you are not inside a clone.
Because a report leaves your machine for the Fabric team, privacy is enforced before anything is sent:
- Secrets in any attached logs are scrubbed on your machine (tokens, keys,
KEY=valueassignments) before they enter the payload. The hub scrubs again as a backstop. - The CLI prints a preview of the exact payload (title, body, and the whole context envelope) and asks you to confirm.
- Log attachment is opt-in (
--attach-logs); nothing about your checks is sent unless you ask. - Under
--jsonor a non-interactive stdin there is no prompt, so sending requires an explicit--yes. A report is never sent silently.
When fabric checks reports a failure it prints a one-line hint pointing at fabric report --from-last-error --attach-logs, so the fastest path to telling the Fabric team is right where
the failure showed up.
Watching events
Instead of polling each surface, watch the hub’s event stream over a long-lived SSE connection. The hub pushes each new event the instant it lands, filtered server-side.
| Command | What it does |
|---|---|
fabric wait --change <id> --on '<selectors>' | Block until the first event whose type matches a selector glob (comma-separated globs over the dotted vocabulary, e.g. check.*, comment.*,review.*, *), print it, then exit 0. |
fabric wait --change <id> --until <outcome> | Block until the change reaches a terminal outcome, then encode it in the exit code: accepted 0, rejected 2, abandoned 3. |
fabric wait … --background | Detach the wait: print the child pid (and an output file) and return immediately, so a harness can watch that pid and wake a dormant agent when it exits. |
fabric events | Stream every event continuously (--json for one object per line), for a supervising daemon that tails the whole feed. Narrow with --change <id> and filter with --on '<selectors>'. |
Both commands remember the highest event cursor they delivered (saved per change) and
resume from it on the next connection, so an event that landed while nothing was connected
is replayed rather than missed. A first-ever wait starts live at the current tip; pass
--since <cursor> to start from a specific point, or --from-start to replay the whole
history. The event types and each event’s data shape are documented in the
agent guide.
Onboarding & history
| Command | What it does |
|---|---|
fabric repo status | Show what’s currently in trunk (empty, or a file count + the most recent change). Run it before importing. |
fabric import <folder> | Replace the repo with an existing Git project’s full history (runs git fast-export for you). Advances trunk and relabels the repo from the folder name; prints the next step. Warns before replacing a non-empty repo. |
fabric import <folder> --force | Replace a non-empty repo without the confirm prompt (required in --json / non-interactive mode). |
fabric import <folder> --into <path> | Nest the imported project under a subfolder. |
fabric clone --at trunk | Read-only checkout of the current trunk frontier (how to view an import’s result). |
fabric clone --at op:<actor>:<seq> | Read-only checkout at a past frontier (time-travel). |
Diversion to object storage is by size only: files over the cap divert to LFS pointers; a small binary imports inline and round-trips byte-for-byte.
Keeping the CLI current
| Command | What it does |
|---|---|
fabric --version | Print the installed CLI version (a short git commit on shipped builds, dev on an unstamped local build). |
fabric version | Same as --version; add --json for {"version", "build"}. |
fabric upgrade | Download the latest CLI this hub serves and replace the running binary in place. No login needed. |
fabric upgrade --force | Reinstall even when the versions already match. |
fabric update is an alias for fabric upgrade. The hub cross-compiles the CLI
on every deploy, so upgrading keeps you in sync with the hub you talk to. It
resolves the hub from --hub, FABRIC_HUB_URL, or your saved login.
When your installed CLI differs from the build the hub serves, any command that
talks to the hub prints a one-line reminder on stderr (it never touches --json
output) telling you to run fabric update. That way a CLI lagging behind a
newer hub, missing a command the hub already documents, tells you so instead of
failing cryptically.
Conventions
--jsonon any command. Default output is human-readable text;--jsongives the same result as structured data for scripting and agents.- Every command prints a link. Commands that act on a change end with a
View: <url>line you can open to see it in the browser. Under--jsonthe same link is in theweb_urlfield instead. - Op refs are
actor:seq: a frontier coordinate, not a content hash. - Idempotent push. Re-running
pushonly appends what’s new; “nothing to do” means your tree already matches the change tip. - Clone never clobbers.
fabric clonerefuses a target that already holds files and writes nothing, so you never lose local work by cloning over it. Pass--forceto write over it anyway (same-path files overwritten, unrelated files kept). Under--jsona refusal is{error: "clone_target_not_empty", message, suggestion, dir}with a non-zero exit. - Stale working dir. Once the change you cloned is accepted, abandoned, or
deleted,
pullstops with the reason plus a next step (fabric clone --at trunkto read trunk, orfabric change createthenfabric cloneto keep editing). With--jsonthe same reason rides in{error, message, suggestion}.statusis offline, so it flags only a working dir whose.fabric/state.jsontracks no change at all.
The definitive, always-current command set is
fabric --help. This page mirrors it for reference. When in doubt, trust the CLI.