Skip to content
Download PDF

Connect a Live Agent ​

The enforced gates only do their job when something feeds them. Wiring your coding host to feed and gate is the last mile of harness engineering. In Fowler's terms, this is the user's outer harness: the model executes, and the hooks feed the sensors and enforce the gates.

Hooks call the gates blind ​

The per-change gate commands auto-resolve the active change when --change is omitted (see Session timeline). That means a hook can call forge trace record or forge budget record without ever knowing the change name — Forge resolves the single active change under .forge/changes/.

Ready-to-paste Claude Code hooks ​

Add this to your ~/.claude/settings.json (or a project .claude/settings.json):

json
{
  "hooks": {
    "PostToolUse": [
      { "matcher": "*", "hooks": [
        { "type": "command", "command": "forge trace record --kind tool --summary \"agent tool use\" >/dev/null 2>&1 || true" },
        { "type": "command", "command": "forge budget record --tool-calls 1 --max-tool-calls 500 >/dev/null 2>&1 || true" }
      ]}
    ],
    "Stop": [
      { "hooks": [ { "type": "command", "command": "forge verify --change \"$(basename \"$(ls -dt .forge/changes/*/ | grep -v /archive/ | head -1)\")\" || echo 'forge: verify gate — review before continuing'" } ]}
    ]
  }
}

What each hook does ​

  • PostToolUse appends a tool event to the timeline and increments the tool-call budget on the active change. The event is attributed to the git identity (see Team collaboration). The --max-tool-calls 500 gives the budget a ceiling to check against — set --max-tokens / --max-usd too if your host exposes those numbers. Because this line feeds best-effort with || true, the ceiling is recorded but not surfaced here; the Stop gate below is where a non-zero exit actually reaches the human.
  • Stop runs the verify circuit-breaker against the most recent non-archived change when the agent stops.

|| true vs. deliberate non-zero

The || true on the feeding commands keeps best-effort feeding from ever interrupting the agent — a failed log write must not derail a run. The enforcing gates deliberately do not get || true: the verify gate is meant to return non-zero so the host surfaces it and the human reviews before continuing.

The Stop hook and OpenCode's session.idle: clarify-gate ​

Forced interactive clarification wires a second, lighter-weight sensor into the same lifecycle-end moment. forge install also appends forge clarify-gate to Claude Code's Stop array, next to the verify circuit-breaker entry above:

json
{ "hooks": { "Stop": [
  { "hooks": [ { "type": "command", "command": "forge verify --change \"$(basename \"$(ls -dt .forge/changes/*/ | grep -v /archive/ | head -1)\")\" || echo 'forge: verify gate — review before continuing'" } ] },
  { "hooks": [ { "type": "command", "command": "forge clarify-gate --cwd \"${CLAUDE_PROJECT_DIR:-$PWD}\"" } ] }
] } }

No || true and no || echo here either — same reasoning as the verify entry: clarify-gate is meant to return non-zero so Claude Code surfaces it. Unlike verify, it is not an enforced gate: it warns on an unresolved clarifying-question marker and lets the session continue either way. See Interactive Clarification for the marker lifecycle and the honest trust-gap caveat around the answer path.

OpenCode has no Stop hook, so forge install wires the structural equivalent instead — a session.idle plugin at ~/.config/opencode/plugins/clarify-gate.ts that execFiles forge clarify-gate --session-id <id> --format json when the agent's turn ends, and surfaces any warning as a toast rather than a console print.

Two of twelve harnesses

This wiring exists for Claude Code and OpenCode only. The other ten supported harnesses carry the canonical callout Guide in their assets, but no Stop/session.idle-equivalent Sensor reads the marker at all — see Interactive Clarification for the full gap.

The richer alternative: forge hook post-tool-use ​

Once wired, prefer the single forge hook post-tool-use command. It reads the host's payload on stdin and extracts the real tool name, so your timeline records which tool was used instead of a generic "agent tool use". Use it in place of the two hand-rolled PostToolUse commands when your host delivers a structured payload.

Marking session boundaries: forge hook session-start ​

If your host also exposes a SessionStart lifecycle event, wire it to forge hook session-start:

json
{ "hooks": { "SessionStart": [ { "hooks": [ { "type": "command", "command": "forge hook session-start" } ] } ] } }

PostToolUse events only exist once a tool has already run, so they can't tell the timeline when a distinct session began — gaps between tool-call timestamps don't reliably distinguish "the model thought for a while" from "the agent was restarted". session-start records a session event on the active change's timeline exactly once per session, without touching the tool-call budget.

The honest limit ​

Tool-call count is reliably reportable from a PostToolUse hook — one call, one increment. Token and dollar spend need host support: only your host knows real usage. If it exposes those numbers, pipe them into the ledger:

sh
forge budget record --tokens <n> --usd <x>

Feed first, then enforce

Start by feeding the timeline and tool-call budget — that works everywhere. Turn on token/$ enforcement only once your host can report the numbers to feed it. See Spend budget for the enforcement semantics.

Next steps ​