---
description: "What the Claude Code, Codex, and OpenCode plugins wire up — workflow hooks, three skills, and the expert subagent."
---

# Agent integration (/docs/agent-integration)

The git-span plugin is how an agent operates git-span without being told the command syntax on every turn. Once installed (see [Getting started](/docs/getting-started)), it ships four groups of capabilities:

* **A touch hook** — fires after supported tool calls that can touch tracked files, healing positional drift inline.
* **A commit advisor** — fires before `git commit`/`git push`, holding the command once when the changeset carries real span debt, and reports (without holding) on a plain `git status`.
* **Three skills** — `git-span` for everyday declaration, inspection, and focused reconciliation; `reconcile` for multi-span triage; and `hook-effect-analysis` for measuring whether hook emissions earn their context cost. Skills load when the task matches; the measurement skill is not part of ordinary span maintenance.
* **An expert subagent** — a dedicated agent for span work: creating, reconciling, and inspecting spans, with judgment about when a coupling is real and when a span should be deleted rather than patched.

The CLI itself doesn't change: every capability the plugin uses is a plain `git span` invocation, scriptable the same way in a terminal or CI.

The bundles also include lifecycle and planning hooks that attribute shell or patch writes reliably, clean up per-session state, and check CLI/plugin compatibility. Their exact event names differ between Claude Code and Codex; the touch hook and advisor are the two user-facing workflows.

## Touch hook + advisor

* **Touch hook** (`PostToolUse`) — fires synchronously after supported file-affecting tool calls. Claude Code covers `Read`, `Edit`, `Write`, and `Bash`; Codex covers `apply_patch` and its shell/exec tool family. It re-anchors pure positional drift (a line-shift from the edit) against the file's real post-edit content, silently — no output, nothing in the transcript, because there's nothing left to act on. If genuine semantic drift survives that heal (the anchored content no longer matches what a span asserts), it injects a bounded context block naming the span, its status, and what to do. Each `(span, status)` pair surfaces at most once per session.
* **Advisor** (`PreToolUse` on the shell tool) — fires before `git commit`/`git push`/`git status` runs. It resolves the actual changeset (staged files, plus tracked-modified files under `-a`/`-am`; for `git status`, staged plus tracked-modified, the same working-tree picture `git status` itself prints), reruns a scoped `git span drift --fix`, and classifies what's left. For `git commit`/`git push`, real span debt — semantic drift, or a changed file no span covers — holds the command with a checklist once per distinct debt state, so the report gets read; an identical retry proceeds. The advisor reports rather than enforces, so a `git commit`/`git push` whose exact debt state a prior `git status` already surfaced in full also proceeds, without holding on a state the agent has already been shown. `git status` is never held: its first preview shows the same checklist as advisory context, later previews show only newly discovered paths or drift rows, and an unchanged preview stays silent. Positional drift never causes a hold; the touch hook already healed it. If the scan itself fails to complete, the advisor holds nothing on that account — it warns that span debt wasn't verified and lets the command proceed, carrying the failed command's own stderr as a delimited `<git-span-error>` block inside the warning so the raw diagnostic is clearly bounded.

> [!NOTE]
> Both hooks fail open on everything that decides *whether* there is something to say: a missing `git span` binary, a timeout, or an unexpected result resolves to "allow silently, inject nothing." Neither can brick an edit or a commit on its own failure.
>
> Rendering is the deliberate exception, and fails **closed**. If the anchor tree can't be drawn, the hook falls back to the flat bullet form and still holds — a defect in how a hold is presented must cost presentation, never the hold itself. That's why those `try/catch` blocks sit around the render calls rather than deferring to the advisor's outer fail-open catch: they exist precisely to keep a formatting error from converting a correctly computed hold into a silent allow. Treat them as load-bearing, not as fallbacks that escaped the rule above.

### What the advisor holds on

Two shapes, both rendered into the held command's `permissionDecisionReason`:

**Semantic drift** — the full span (all anchors, drifted ones labeled, the description), held on once per distinct set of findings; a retry with the same findings proceeds, and editing a span's anchors changes the findings and produces one fresh hold:

```
This change leaves an implicit dependency out of date:

## checkout-flow
├─ web/checkout.tsx #L4-L6 — changed
└─ api/charge.ts    #L30-L76

Checkout request flow that carries a charge attempt from the browser to the
Stripe-backed server.

---

Dispatch a forked subagent to bring the coupled files back into agreement
(follow confirmed authority) — preserve anchor shape; if an address changed,
swap the old anchor for the new one with `git span replace`; update or
retire the why only if its meaning changed; require `git span drift
checkout-flow` to report zero. Then retry. Load the `git-span:reconcile`
skill in the fork. The hold will not fire again for the same debt state.
Conform a side only when confirmed authority or a satisfied gate decides it;
report ambiguity or an obsolete dependency.
```

The closing copy uses the Claude harness's vocabulary (`Agent` with `subagent_type: "fork"`); under Codex the same direction is spelled `spawn_agent` with `fork_turns: "all"`.

The `## <name>` header and the why sentence are the same shape `git span list` prints; the anchor list deliberately is not — where the CLI prints a flat `- path#Lstart-Lend` bullet run, the hook renders the span's full declared anchor list as a box-drawing tree grouped by shared path prefix. A directory holding a single entry folds onto that entry's line, so a branch only ever appears where two or more anchors actually share a prefix. A whole-file anchor is a bare path with no range column; where the same file also carries line ranges, it takes `(whole file)` in the range column so its own drift label can never be read as belonging to a neighbouring range. The CLI's own text format is untouched — only the hook's re-presentation of it groups, so `(whole file)` is a token you will see in a hold and never in `git span list` output.

**Uncovered writes** — a changed file no span anchors at all, held on once per distinct debt state; an unchanged retry passes, and so does a commit/push whose exact debt state a prior `git status` already showed in full. When the rest of the changeset already belongs to spans, a related-spans section follows the checklist — only anchors that are part of *this* changeset, as line ranges wherever possible, ranked so the span covering the most of the changeset leads (ties broken by directory proximity to an uncovered file, then by name), capped at eight spans with any remainder disclosed by count, each group followed by that span's `why` sentence when it has one recorded:

```
<git-span>
- src/new-module.ts

Dispatch a forked subagent to determine if this file carries implicit
dependencies and to then use `git span` to document them:

`git span add <name> <anchor> [<anchor>] ...`  — an anchor is a path or a `path#Lstart-Lend` range
`git span why <name> "<why>"`

The "<why>" is one or two complete present-tense clauses stating the
relationship and any decisive nonlocal authority, invariant, permitted
difference, lifecycle state, evidence gate, or focused conditional verification.
Labels are optional but must introduce complete clauses. Omit generic work
orders and CLI procedure.

---

Other files in this change already belong to spans — an uncovered file above
might belong with one of these instead of a new one:

## checkout-flow
└─ web/checkout.tsx #L4-L6

Checkout request flow that carries a charge attempt from the browser to the
Stripe-backed server.

If none exist, retry the command to proceed (one-time check).

Load the `git-span:git-span` skill in the fork.
</git-span>
```

`MOVED` and `RESOLVED_PENDING_COMMIT` anchors are never debt — they never appear in either checklist and never cause a hold. `.span/**` writes are excluded from the uncovered-writes check so a span repair riding the same commit never self-triggers the advisor.

A `git status` preview renders the same two checklists as advisory `systemMessage` context, minus each's retry phrasing — semantic drift drops `— then retry` from its closing sentence, and uncovered writes drops the whole `If none exist, retry the command to proceed (one-time check).` sentence — nothing was ever held, so there's nothing to retry. Each preview memoizes the individual uncovered paths and semantic drift rows it names. Later previews render the full checklist for only newly seen items; when every current item was already named, the preview is silent. This per-item preview memory is separate from the once-per-debt-state *hold-credit* memo that `git commit`/`git push` rely on, so status cannot spend the one-time hold a later real commit depends on. Status also marks its complete debt state as already explained on a separate axis, so a `git commit`/`git push` that follows a `git status` on the same unchanged state passes instead of holding — the advisor only reports, and there's nothing left to tell the agent that the status preview didn't already say.

### Suppression

There's no override for an advisory hold, and none is needed — every hold resolves on its own: a fresh finding holds once, and an identical retry proceeds. What's left to configure is what the hooks pay attention to in the first place:

* `.span/.hookignore` — a gitignore-like, per-path file suppressing the touch hook's context injection for noisy spans (wiki/marketing prose, etc). Never affects `git span` command output itself.
* `.span/.advisorignore` — user-owned; each non-comment line is a gitignore-style path pattern (same grammar as `.hookignore`, minus the span-slug-prefix list) excluding matching paths from the advisor's uncovered-writes check. Nothing auto-creates it, and a missing/unreadable file fails open to no additional exclusion. It never suppresses the semantic-drift check. This is the right home for generated output a listed path will never carry a span for — the advisor cannot recognize a regenerated bundle on its own, and spanning build output is the wrong answer.

Separately, and with nothing to configure: the uncovered-writes list omits files whose change is recognizably mechanical, so a release bump across twenty manifests doesn't bury the file carrying the real edit. Lockfiles, minified output, sourcemaps, and vendored trees are dropped by path; a manifest whose diff moves nothing but a version token, checksum, or timestamp is dropped by content. Source and prose files are never dropped by content — an unrecognized file type can only ever stay listed. Because a suppressed file is simply one you're never told about, set `CLAUDE_CODE_HOOKS_LOG_FILE` (`CODEX_HOOKS_LOG_FILE` under Codex) to a path when you need to know why something is missing: each invocation records how many candidates were dropped by path, by content, and whether the diff read succeeded. Unset, that record has nowhere to go.

### Resolving a held commit

1. Semantic drift: restore each relationship using confirmed authority or a satisfied gate, reconcile its why and anchors, and require scoped zero drift before retrying. An unchanged retry proceeds because the same findings are held only once.
2. Uncovered writes: either declare the coupling (`git span add` then `git span why <name> "..."`) or just retry — a second attempt at an unchanged debt state passes.

See [Reconcile drifted spans](/docs/guides/reconcile-drifted-spans) for the full triage workflow.

## Codex specifics

The trust flow in [Getting started](/docs/getting-started) is what actually turns these hooks on under Codex — installing the plugin alone leaves them inert until `/hooks` review. Two things worth repeating here because they change what "installed" means:

* Hash-stable hook filenames keep an existing trust decision valid across plugin upgrades, so a Codex user doesn't re-trust on every update.
* Hooks are disabled entirely on Windows, with no partial fallback — none of the touch-hook or advisor behavior above applies there.

None of this affects Claude Code, which has no separate trust step.

## OpenCode specifics

OpenCode loads the plugin from the npm package (see [Getting started](/docs/getting-started)) with no trust-review step, and runs its hooks in-process. The touch hook and advisor behave as described above, with these differences:

* **A hold is a thrown error.** There's no deny decision and no separate system message: when the advisor holds `git commit`/`git push`, the before-hook throws, the command never runs, and the thrown checklist is the error the model sees verbatim. The hold is still advisory and one-time — a bare retry passes.
* **Injection appends to tool output.** `<git-span>` blocks and `git status` previews are appended to the tool result after execution, never rewritten or held back.
* **Host-level failures get no attribution (degraded parity).** OpenCode's after hook fires only on successful execution, and only host-level failures skip it — invalid arguments, denied permission, a spawn error. Those calls' writes are neither healed nor attributed until a later successful read/edit touches those paths, and any report stashed for them is dropped silently. A command that merely exits nonzero still runs the after hook: the bash tool returns a numeric `exit`, and join semantics gate each write on that exit (a short-circuited `&&` attributes nothing because its write never ran). As with the twins' failed commands, though, it can still surface a write only when the parse carries decisive post-state evidence, such as an expected replacement result — an inconclusive write goes silent under a nonzero exit instead of firing as an existence-gated advisory. Aborts and timeouts surface as `exit: null`, suppressing attribution exactly like the twins' interrupted rows. As everywhere, `git span drift` in CI ([The enforcement backstop](#the-enforcement-backstop)) is what catches what the hooks miss.
* **No per-hook timeouts.** OpenCode doesn't kill slow hooks, and plugins run in-process — an advisor scan briefly blocks the host loop — so the plugin bounds its own `git span` subprocesses with tighter timeouts than the other twins' hosts impose.
* **Coverage.** The hooks cover the `bash`, `read`, `edit`, and `write` tools, plus `apply_patch` on gpt models (where it replaces `edit`/`write`). OpenCode's experimental code-mode `execute` tool is excluded from both hooks in v1 — commands issued through it bypass attribution and advisory checks.
* **Vocabulary in closings.** Hold checklists dispatch follow-up work through the Task tool (`task` with `subagent_type`) and address skills by bare directory names (`reconcile`, `git-span`) via the skill tool — rather than the Claude or Codex spellings shown in the examples above.
* **Update checks are off for the session.** Loading the plugin disables git-span's update check for the whole OpenCode process, so every `git span` invocation made from inside the session stays silent about available updates.

Set `OPENCODE_GIT_SPAN_LOG_FILE` to a path when you need the JSONL hook log — the churn-suppression records described under [Suppression](#suppression) go there under OpenCode.

## The enforcement backstop

The touch hook and advisor only run inside a hooked agent session. A human-authored commit, a session with hooks disabled, or a Codex `permissionDecision: 'deny'` that doesn't actually block can all still land span debt. [CI & git integration](/docs/ci) covers `git span drift` as the backstop that catches what the in-session hooks miss.
