fwdcentaur with Claude Code

A how-to for a person who runs fwd on their own machine, with Claude Code and the fwd plugin.

In short

# once per machine, in a fwdcentaur checkout
scripts/build-web.sh && cargo install --path crates/fwd
claude plugin marketplace add /path/to/fwdcentaur
claude plugin install fwd@fwdcentaur

# once per repository
cd /path/to/your/repo
fwd init --handle <your GitHub login>

# then start Claude Code there, and tell it that it leads the project through fwd
claude

The lead names itself, starts a triage agent as a background subagent, and hands tasks to implementers, each a subagent with a user of its own. You answer what they ask on the page (fwd ui), in VS Code, or in the terminal.

1. Installing fwd

fwd is one binary, fwd: the command, and the daemon, which the first command that needs one starts from that command's own executable. It has to be on your PATH, and on the PATH Claude Code runs its hooks with.

1.1 Building it from a checkout

fwd is built from a checkout of its repository. You need read access to the repository on GitHub, which is private, and git, rustup and trunk (cargo install trunk --locked), which builds the page. The repository pins its Rust toolchain in rust-toolchain.toml, 1.98.1 with the wasm32-unknown-unknown target: rustup installs it the first time a build in the checkout needs it, and that first build compiles everything, which takes several minutes.

git clone https://github.com/synestheticsystems/fwdcentaur.git
cd fwdcentaur
scripts/build-web.sh               # builds the page into crates/fwd-web/dist, which the next build embeds
cargo install --path crates/fwd    # puts fwd, with the page inside, in ~/.cargo/bin
fwd --version

Keep the checkout where it is: the plugin is installed from it (section 3.1). A fwd built without the page first shows, at fwd ui, the commands that build it instead of the page.

1.2 Upgrading

One daemon serves every project of your user, and it keeps running whatever binary started it. So after installing a new build, stop it, and the next command starts the new one:

git pull
scripts/build-web.sh && cargo install --path crates/fwd
fwd daemon stop

A restart is quiet: an agent's fwd watch reconnects by itself, and the VS Code extension joins the daemon again by itself. A command waiting on the daemon when it stops ends within ten seconds with exit 7, saying what to do (section 12).

2. Setting up a project

fwd knows a repository by its upstream, the host and path of its origin remote (git@github.com:acme/widgets.git and https://github.com/acme/widgets.git are both github.com/acme/widgets), never by a directory, so every clone and worktree of it belongs to one project. Give the repository an origin remote first: without one, none of its lines can be commented on.

cd /path/to/your/repo
fwd init --handle <your GitHub login> --display-name "<your name>"

fwd init joins the project the repository already belongs to, or creates one named after the directory (--name chooses another). It writes a marker, .fwd/project.toml, at the checkout's root; stores your handle, once for the machine; starts the daemon if none runs; and adds you to the project as its person. It makes no agent: each agent names itself (section 4). Commit .fwd/project.toml if a clone on another machine should find the project.

A second clone or a worktree of the repository needs nothing more, and another repository joins the same project with fwd init --project <its name>. From then on fwd whoami shows whom a command acts as, in which project, checkout and scope.

3. The Claude Code plugin

The plugin exists so that each Claude Code agent on your machine has a user of its own in the project, and what an agent writes is attributed to that agent, not to its lead or to you. It has one hook, which stamps each fwd command an agent runs with the ids Claude Code gives that agent, and it brings a skill for the lead, one for the triage agent and one for implementers: fwd:fwd-lead, fwd:fwd-triage and fwd:fwd-implementer.

3.1 Installing it

Once per machine, from the checkout:

claude plugin marketplace add /path/to/fwdcentaur   # the checkout, not a worktree that may go away
claude plugin install fwd@fwdcentaur               # user scope: every project on this machine

The checkout's .claude-plugin/marketplace.json is a marketplace named fwdcentaur that lists one plugin, fwd, kept in integrations/claude-code. Nothing is added to any project's repository.

3.2 Checking it works

  1. claude plugin list shows fwd@fwdcentaur enabled, and claude plugin details fwd lists the hook and the three skills. scripts/refresh-plugin.sh --check, from the checkout, says whether the copy sessions run matches the checkout (section 3.4).
  2. In a new session, ask Claude to run fwd whoami. Its subject is claude-code:<the session's id>/main, and in a subagent the subagent's own id stands in place of main. Its principal is null until the agent names itself.

3.3 What the hook stamps, and what it approves

Before each Bash command, the hook puts into the command's environment the session's id and, inside a subagent, the subagent's (FWD_CLAUDE_SESSION, FWD_CLAUDE_AGENT). fwd reads the two as the command's subject, claude-code:<session>/<agent>, and acts as the principal bound to it: the one that agent named for itself. No --as is needed, and one naming anyone else is refused.

The hook approves the commands it stamps, without a permission prompt.

Claude Code applies a hook's change to a command only when the hook also approves the command, so the hook stamps only a command whose approval gives away nothing but fwd: fwd invocations, cd, and the filters head, tail, grep, wc, jq, sort, uniq and cut reading fwd's output, joined by |, &&, || or ;, with no $ or backtick outside single quotes, no globbing, grouping or background, and no redirection but 2>&1 and to or from /dev/null. It never stamps fwd admin, daemon, init, ui or project, which act for the machine or its person. Any other command, and any call that asks to run outside Claude Code's sandbox, goes through the permission flow as it would without the plugin, and the deny and ask rules in your settings still apply.

In a session one of whose agents is bound, a write in a command the hook left alone is refused unless --as is typed on it: fwd cannot tell which of the session's agents ran it, and FWD_AS does not tell it, since every agent of the session inherits it. So agents run fwd on its own, or piped into one of those filters, with text in single quotes, as the skills tell them; in a script, or in a shell of your own, type --as <name> on the command (Claude Code without the skills says more).

3.4 Updating it

claude plugin install copies the plugin into Claude Code's plugin cache, ~/.claude/plugins/cache/fwdcentaur/fwd/<version>, and sessions run that copy, not the checkout. So after pulling a fwdcentaur that changes the plugin, its hook or its skills, refresh the copy, from the checkout:

scripts/refresh-plugin.sh           # claude plugin update fwd@fwdcentaur, then the copy compared with the checkout
scripts/refresh-plugin.sh --check   # the comparison alone, which changes nothing

The script runs claude plugin update fwd@fwdcentaur, then compares the copy with integrations/claude-code in the checkout, file by file; it names each file that differs, and exits 1 if one does. The update copies the plugin again only when its version has changed. The plugin's manifest names no version, so Claude Code takes the commit the checkout is on as the version, and a pull that moves the checkout to another commit gives the update a new version to copy, into a directory named after the commit. An edit to the plugin that is not committed moves no version, so the update leaves the copy as it was, and the comparison names the edited files. A session picks up the new copy when it starts, or at /reload-plugins. What the hook stamps is decided by the fwd binary (fwd session stamp), so that part follows cargo install.

4. Roles and names

So that the graph says which agent did what, and a question reaches the one agent whose job is to answer it first, every agent selects one of four roles for its user (docs/DECISIONS.md, D113: an agent's role is one of lead, triage, implementer or reviewer). You hold none: you are the project's person.

lead
A session that coordinates the project's work with you: one for each session or worktree that does so, and one for a Claude Code session, its own thread (docs/DECISIONS.md, D119: one lead per worktree or top-level agent session, not per project). The agents a lead dispatches select another role.
triage
Answers first for the scopes it serves: the questions asked of no one in particular about their work, the comments on their tasks and code, proposals and reviews reach it. A scope has one, so that its questions have one front line, and in a small project with small scopes one may serve the whole project instead (docs/DECISIONS.md, D122: triage is per scope, not per project).
implementer
Works tasks in a scope. As many as the work needs.
reviewer
Reviews what it is asked to review. As many as the work needs.

An agent names itself and selects its role in one command, before it writes anything:

fwd principal add <name> --role lead    # or triage, implementer, reviewer

With the plugin, the command binds the new principal to the agent that ran it, and every fwd command that agent runs from then on acts as it. Until an agent has named itself, its writes are refused, with the command to run.

A name is a label the agent chooses; the principal's id is what identifies it, and everything that refers to a principal stores the id. Choose a name no other principal of the project has (a role and a qualifier reads well: implementer-retry); fwd principal list --all lists the names taken. A name two principals share is refused wherever it is typed, listing each by the first eight characters of its id, which then names the one meant; fwd principal rename <new name> renames an agent, keeping its id, its work and its inbox.

5. The lead and its triage subagent

To have a session lead the project, start claude in a checkout of the repository and tell it that it leads the project through fwd; the fwd:fwd-lead skill covers that. Its first command names it, fwd principal add <a name> --role lead. From then on it coordinates the work with you: it plans the work, hands it to implementers, looks at what they hand back, and records what you rule.

Its first job is to start a triage agent for the scope it coordinates. The questions asked of no one in particular, the comments on tasks and on code, proposals and reviews reach the triage agent of the scope they concern, else the one that serves the whole project; without one the questions wait on the Questions page, listed under their scope, and a lead that answers them itself fills its context with first-line traffic. fwd says so, with the steps, when an agent selects lead where no triage agent answers first for the scope it works in, and again when a lead's inbox, or a question asked of no one, meets such a scope. In a checkout that finds no scope, as in a new project, it reads:

This checkout finds no scope, and no agent serves the whole project, so no triage agent answers for what concerns no scope. The questions asked of no one in particular, comments on tasks and on code, proposals and reviews reach the triage agent of the scope they concern, else the one that serves the whole project; without one the questions wait on the Questions page, listed under their scope, and a lead that answers them itself fills its context with first-line traffic. To make one, spawn a background subagent briefed with the fwd-triage skill (integrations/claude-code/skills/fwd-triage in the fwdcentaur checkout) and told the project and a checkout of the scope to work in: its first command names it, with a name no other principal of this project has, and selects triage for the scope, fwd principal add <its name> --role triage --scope <scope id> (without --scope it takes the scope its checkout finds), which binds that principal to the subagent, whose commands the fwd plugin stamps, so that it acts as itself from then on, and tells it of the questions that waited there. A small project with small scopes may instead endorse one triage agent for the whole project, --project-wide in place of --scope; a large one keeps one per scope. A triage agent relaunched later names itself as the old one's successor, fwd principal add <its name> --succeeds <old name>, which moves its scopes and its mail to it.

Where the checkout finds a scope, the first sentence names it instead, with the first eight characters of its id, and the command carries that id. So the lead spawns that subagent, telling it the scope to serve, or in a small project to serve the whole project, and the fwd:fwd-triage skill tells it the rest. It works in passes: fwd inbox pop, then for each notification the command that settles it. It answers what the graph can answer, in the question's thread; forwards to you, or to an implementer, what is theirs; links a comment to the tasks it concerns; files a suggestion as a proposed task; approves or rejects proposals; and accepts or returns a submitted task when the evidence plainly settles it, asking you otherwise. It keeps within caps each pass (10 links, 5 new tasks, 3 approvals, rejections or reviews, and 20 replies), asks when a comment's meaning is unclear, and never starts a task or edits code. Between passes it waits for the next notification with fwd watch --count 1 --format json run in the background, which exits when one arrives, or runs its pass on a timer with /loop 2m.

A scope has one triage agent: where another lead's serves the scope already, a second subagent for it is refused, naming that agent, and stops, and that agent answers for the scope. A question asked of no one while no triage agent answered for its scope reaches the agent that comes to answer for that scope, once, the moment it does, oldest first. A triage agent that serves nothing hears nothing for its role, and each of its commands says so, naming the commands that bind it to a scope or endorse it for the whole project (section 4). A triage agent relaunched after a session limit names itself as the successor of the one it replaces, and takes its scopes.

6. Implementers

A lead hands a task to an implementer, usually a subagent in a git worktree of its own, on its own branch; the fwd:fwd-lead skill says what the lead's brief tells it, and the fwd:fwd-implementer skill the rest. Each implementer has a user of its own and works in the graph as itself: it claims its task, comments on it and asks its questions under its own name. In commands, an implementer's job reads:

fwd principal add implementer-retry --role implementer   # it names itself first
fwd scope attach-dir <scope id>        # when its checkout's branch names no scope
fwd task start <task id>               # or: fwd task next --claim
fwd comment <task id> 'what I found or did'
fwd ask --to <the lead's name> --about <task id> 'a question for the lead'
fwd ask --about <task id> 'a question for no one in particular'   # the triage agent of the task's scope answers
fwd task create 'Add jitter to the backoff' --under <task id> --label feature --why 'clients reconnecting together stampede the server'
fwd inbox pop                          # between its steps, and before it finishes
fwd task submit <task id> --evidence 'commit 1a2b3c; tests green'
fwd principal retire implementer-retry # its last fwd command

A scope is an area of work: what the work in it is trying to achieve, and the branches it happens on. A command finds the scope from its checkout: the scope the checkout is bound to (fwd scope attach-dir), else the one active scope naming its branch. For work no scope covers, the implementer makes one: fwd scope create '<name>' --intention '<what the work is trying to achieve>'.

A subagent reads its inbox with fwd inbox pop between its steps and before it finishes, since Claude Code runs the Stop hook, which keeps an agent working while its notifications are unread, for the session's own thread alone.

fwd task submit moves a task that needs review to in review and asks the triage agent of its scope, else the one that serves the whole project, which accepts what the evidence settles and asks you about the rest; any other task goes straight to done. No one accepts their own submission.

When its job ends, a subagent that wrote to the graph retires its own user, as its last fwd command before it reports, so that the page's pickers stop offering it (docs/DECISIONS.md, D123: a subagent retires its own user when its job ends); one that only reports to its lead writes nothing to the graph and names itself not at all. A subagent resumed later brings its user back with fwd principal add <the same name>, and the lead retires any left behind when it accepts or merges their work, since nothing retires a user by time: fwd cannot tell a quiet agent from a gone one.

7. The inbox hooks

The plugin's hook stamps Bash commands only. Four more hooks put an agent's notifications into its context between its steps, and keep it working while any are unread. They are for an agent that runs as a Claude Code session of its own: a lead above all, or a triage agent that is not a subagent.

They go in that checkout's .claude/settings.local.json, kept out of git. Not in the plugin, since a hook in every session on the machine would run fwd, and fail, in every project with no fwd graph; and not in the shared .claude/settings.json, which reaches everyone who works in the repository, whether fwd is installed or not. Copy the hooks block of the plugin's settings.example.json (in integrations/claude-code), merging it into the file if it exists:

{
  "hooks": {
    "SessionStart": [
      { "hooks": [ { "type": "command", "command": "fwd prime --format hook" } ] }
    ],
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "fwd inbox pop --format hook --hook-event UserPromptSubmit" } ] }
    ],
    "PostToolBatch": [
      { "hooks": [ { "type": "command", "command": "fwd inbox pop --format hook --hook-event PostToolBatch" } ] }
    ],
    "Stop": [
      { "hooks": [ { "type": "command", "command": "fwd session stop-check --format hook" } ] }
    ]
  }
}
Hook eventWhat happens
SessionStartfwd prime puts the scope, the ready tasks and the unread notifications into the agent's context at startup, on resume, after /clear and after compaction.
UserPromptSubmitThe notifications that arrived since the agent's last step go into its context and are marked read. Nothing, when none arrived.
PostToolBatchThe same, after each batch of tool calls.
StopWhile unread notifications remain, the agent is kept working; otherwise it may stop. Claude Code lets it stop anyway after eight blocks in a row.

Each hook acts for the agent whose hook fired: Claude Code hands every hook the session's id and, inside a subagent, the subagent's, and fwd acts for the principal bound to that agent. Until an agent has named itself, its hooks act for no one and print nothing. What a hook puts into the context says why each notification is there, and what to do with it:

You have 1 new fwdcentaur notification(s) as impl-alice:
dnorman replied to your question on Retry on disconnect (replied, id z_1_4NVX, priority next, 2m ago)
    30s is fine for now; keep it a named constant
When you have acted on one, reply with `fwd reply <notification id> '<what you did>'`; to leave it, `fwd dismiss <id>`. ...

8. Asking and answering

A question is a message that asks someone something. It always notifies whom it asks, even when they asked it themselves, and the answer comes back as a reply in its thread.

fwd ask --to @<your handle> --about <task id> 'Is Windows in scope?'   # one person or agent
fwd ask --about <task id> 'Should the retry cap be configurable?'     # no one in particular: the triage agent of the task's scope answers
fwd ask --path src/net/ws.rs --lines 120-134 'Why a lock here?'        # about lines of code, which become a highlight

--priority says how soon it wants an answer: now, the default, next or later. An agent acts on each notification it gets, then says what it did, fwd reply <notification id> '<what I did>'; it hands one on with fwd forward <notification id> --note '<why>', to no one in particular, which the triage agent of the scope it concerns hears, or, with --to, to someone in particular; or it leaves one with fwd dismiss <notification id>. Given the notification's id rather than its message's, reply and forward mark it read as they post and print its id as settled, so that a watch armed next does not print it again; a notification dismissed stays dismissed when a reply or a forward names it later. You answer on the page's Inbox and Questions pages, in the VS Code extension's inbox view, or with fwd reply in a terminal.

In any message, @name names a person or an agent, who hears of it; @<id> or [[<id>]], with the whole id or its first six characters or more, names anything, a task or a highlight say, and [[D12]] a decision. A name that fits nothing, or that two principals share, is refused when you post, so a typo never goes out; text in backticks keeps an @ or brackets as they are. To correct a message, edit it where it stands: fwd message edit <message id> '<the whole new text>', which tells only whoever the new text names and the old one did not. Its author edits it, and so does an agent that succeeded its author, so that an agent relaunched to carry on another's job corrects what the other wrote where it stands; the message then shows which agent edited it.

To point a reader at a whole file as you saw it, name its path from the repository's root in double brackets: [[src/net/ws.rs]] as it stands on disk, unstaged changes included; [[src/net/ws.rs@index]] its staged copy; [[src/net/ws.rs@HEAD]] the file at a commit, which a sha, a branch or a tag names as well, kept as the commit it named when you posted (a file at the root with no dot in its name is [[./Makefile]], since a path needs a slash or a dot to be told from an id). fwd reads the file in the checkout the command runs in, and refuses one that is not there at that version, so name a file with the CLI or in a comment in VS Code's editor: the page has no checkout to read it in. It shows as a lozenge naming the path and the version, src/net/ws.rs on disk, src/net/ws.rs index or src/net/ws.rs e13b8e2; a click on it goes, on the page, to the file's highlight, and in VS Code opens the file at that version. Anything a message names, of any kind, is a mention (docs/DECISIONS.md, D125: anything a message names in its text is a mention, whatever its kind), and of those only a person or an agent hears of it.

8.1 Who hears of what

A message reaches only whom it is addressed to, and whom it mentions (docs/DECISIONS.md, D111: a message reaches only whom it is addressed to). No one is told of their own message, except of a question they asked of themselves. The triage agent in the table is the one that serves the scope the message concerns, else the one that serves the whole project (docs/DECISIONS.md, D122: triage is per scope, not per project): a message concerns the scope of the task, the code or the decision it is about, else the scope its writer's checkout finds, and fwd ask --scope <scope id> asks a question of the scope it names.

What is writtenWho is told
A question to someoneThat one, and whoever it mentions. Not the people of the task or the code it is about, who read it when they open its thread.
A question to no one in particularThe triage agent, and whoever it mentions. While no triage agent answers for its scope it waits on the Questions page, listed under its scope, and reaches the agent that comes to answer for that scope when one does.
A comment on a taskThe task's assignee and the triage agent; whoever it mentions.
A comment on lines of codeThe triage agent; whoever is assigned a task anchored within 50 lines of it in the same file (a whole file is within reach of every line of it); whoever it mentions.
A replyWhoever wrote the message it answers, started its thread or wrote in it; for a thread on code or on a task, the people a comment there reaches; whoever it mentions.
A task submitted for reviewThe triage agent; with no triage agent to take it, whoever approved the task, else its creator. Never the submitter.

What one author does on one task while you have not read it, steps added and comments on it, reaches you as one notification, which says how many of what.

8.2 Waiting for an answer

An agent never blocks its turn on its inbox. When it cannot go on without an answer, it runs fwd watch --count 1 --format json as a background command (in Claude Code, a Bash call with run_in_background) and works on anything else meanwhile. The watch has no timeout: it exits when a notification arrives, which wakes the agent, and a daemon restart does not end it.

8.3 Rulings

When you give a ruling that later work rests on, the agent records it as a decision in your words, so that it has a number people cite and so that, when it is repealed or replaced, whoever made something that cites it is told:

fwd decision create 'Retries back off exponentially' --area architecture \
    --decision 'back off exponentially, capped at 30s' --decided-by @<your handle>
fwd cite <task id> D12

An answer that settles one task or one question stays in its thread instead, where whoever picks the work up reads it. fwd decision list, and the page's Decisions page, list the rulings by number.

9. The VS Code extension

The extension exists so that you see in the editor where people and agents are talking about the code, can start such a conversation on the lines you are looking at, and can work through a task's checklist beside the code it points at.

The extension runs the fwd on your PATH, else ~/.cargo/bin/fwd, unless the setting fwd.path names another, and acts as you unless fwd.as names another principal. The extension is a VSIX file, installed with code --install-extension <the file> and a Developer: Reload Window. Building the file from a checkout takes more than fwd does: wasm-pack, Node and npm, and a checkout of an Ankurah branch beside the fwdcentaur checkout, from which the extension's node takes its Ankurah crates by path. The extension's guide, docs/guides/vscode-extension.html in the repository, has the commands and everything else about the extension.

10. The page

fwd ui                    # opens http://127.0.0.1:<port>/?p=<project id> in the browser
fwd ui --no-open          # prints that URL instead
fwd --as <name> ui        # the same page, acting as that principal

The page is where you read and answer. Inbox holds your notifications, worked from the keyboard, with an Ask box for questions about no particular task. Questions lists the open questions asked of you, under the priority each wants its answer at, and the questions asked of no one that wait for a triage agent, under a heading for their scope, Asked of the scope Retries, waiting for its triage agent, or Asked of the project, waiting for triage for those that ask no scope. Tasks lists the tasks with their labels, Decisions every ruling by number, and then come Graph and Scopes. The header picks the principal the page acts as, and the project; a selector beside the Inbox's and the Questions page's headings shows one other principal's at a time, to look without acting as them.

On your network, every device can write.

fwd ui --lan prints a URL a phone on the same Wi-Fi can open. From then on the daemon listens on the network until fwd daemon start --loopback, and since it asks no device who it is, every device on that network can read and write every graph.

11. Your data

The daemon keeps every project in its own SQLite database, in the data directory: ~/Library/Application Support/fwd/ on macOS, and $XDG_DATA_HOME/fwd/, usually ~/.local/share/fwd/, on Linux (FWD_DATA_DIR names another). It holds the only copy of every project's graph on the machine, so back it up:

fwd admin backup                                         # a dated folder in fwd-backups beside the data directory; keeps the newest 7
fwd admin backup --to ~/Dropbox/fwd-backups --keep 30    # any folder, a synced one included

A backup is kept only when every database in it passes SQLite's integrity check. fwd admin restore <backup folder> checks every file of a backup before it writes anything, and writes it back only into an empty data directory that no daemon serves, so stop the daemon and move the old directory aside first. The daemon runs until fwd daemon stop, and fwd daemon status reports on it without starting one.

12. When something is off

Back to the contents