lockstep
9 min readNaman Jain

How cross-vendor agent capture works: hooks, MCP, and instruction files

Every major coding agent exposes the same three surfaces. How Lockstep uses lifecycle hooks, an MCP server, and instruction files to capture agent work.

Two developers on the same team point two different agents at the same system. One runs Claude Code, the other Codex. Agent A renames an endpoint after lunch; agent B spends the afternoon building against the old name. For a coordination layer to catch that, it has to see what both agents do, which means it has to integrate with vendors it doesn't control, without asking either developer to change how they work.

Before writing Lockstep's first line of capture code, we ran a feasibility spike on exactly this question: does cross-vendor auto-capture physically function? The answer turned out to be yes, and more cleanly than expected, because the three major CLI agents have converged on the same three integration surfaces. This post is about those surfaces, how Lockstep uses them, and, honestly, which parts are shipped versus proven-but-not-built.

The three surfaces

Every major coding agent now exposes:

Lifecycle hooks. Shell commands the harness executes at fixed points in the agent loop: session start, before/after a tool call, end of turn. The hook is configuration, not conversation: it fires whether or not the model is paying attention.

MCP tools. A standard protocol for handing the agent structured tools backed by your own server. The Model Context Protocol spec is JSON-RPC with capability negotiation; every vendor speaks it, so one server serves all of them.

An instructions file. A markdown file the agent reads at startup. Behavioral convention rather than enforcement.

Here's the current state across vendors, as of July 2026:

CapabilityClaude CodeCodex CLIGemini CLI
Lifecycle hooksFull set: SessionStart, PreToolUse, PostToolUse, Stop, SessionEnd, and many moreSessionStart, PreToolUse, PostToolUse, UserPromptSubmit, Stop, subagent events; command handlers onlySessionStart, BeforeTool, AfterTool, BeforeAgent/AfterAgent, SessionEnd, model-level hooks
The three events capture needsYesYesYes
MCP toolsYesYes (STDIO + HTTP)Yes (settings.json)
Instructions fileCLAUDE.md + SkillsAGENTS.mdGEMINI.md + extensions

(Sources: the Claude Code hooks reference, the Codex hooks docs, and the Gemini CLI hooks reference.)

One detail in that table is a lesson in itself. When we ran the spike, Gemini's tool hooks were documented as PreToolUse/PostToolUse; they now ship as BeforeTool/AfterTool. The events are equivalent: same firing points, same stdin/stdout JSON contract. But the names moved underneath us within months. Vendor surfaces drift. Any architecture that lets vendor-specific names leak past a thin adapter layer will pay for it continuously.

That's the first architectural rule the spike bought us: a vendor-agnostic core with thin per-vendor adapters. The core never knows which vendor is talking. Each adapter is a hook-config shim plus an instructions file, both pointing at the same MCP server. Draw that seam on day one or neutrality dies quietly.

Why capture is hook-based, not prompt-based

You could try to build capture as pure instruction: put "after you finish a change, summarize it and call notify" in the instructions file and trust the model. Sometimes it works. It is the wrong foundation, for a reason worth stating precisely: instructions are read; hooks are executed.

An instructions file is advisory. The model can deprioritize it under a long task, a context compaction can push it out of working memory, and a fine-tuned behavior change in the next model version can silently alter compliance. If your coordination layer's correctness depends on the model remembering a convention, your correctness degrades with attention, and you'll discover each regression as a missing record, weeks later.

A hook is configuration the harness itself executes. PostToolUse after a file edit fires every time, at the harness level, whether the model is diligent or distracted. So Lockstep is belt-and-suspenders: the instructions file teaches the agent to use the tools well (check the ledger before touching a shared surface, propose a decision when it makes a durable call) and the hooks guarantee the baseline capture happens regardless. The skill makes the agent a good citizen; the hook makes forgetting impossible.

There's a second, less obvious reason: hooks see ground truth. A prompt-based capture records what the model says it did. A hook-based capture runs git diff and records what actually changed on disk. Those differ more often than anyone likes.

What the hooks actually do

lockstep init installs three hooks into Claude Code's settings (the personal, gitignored file; hook config is per-developer, not committed):

SessionStart is the replay path. It pulls the developer's inbox, the binding decisions, and the product-constraint briefing from the ledger, formats them highest-blast-radius first, and returns them as additionalContext, injected into the agent's context before it does anything. This is the shift-change handoff: what changed since you were last here, what's binding, what's delegated to you, which project principles govern your judgment. The agent starts every session already briefed, without being asked to go look.

PostToolUse (matched to file-editing tools: Edit|Write|MultiEdit|NotebookEdit) and Stop are the capture path. On each firing, the hook diffs the working tree against HEAD, locally. From the changed files it extracts canonical surface identifiers: http:POST /auth/session, proto:pkg.Service/Rpc, gql:Mutation.field. Canonical IDs are the load-bearing trick: a consumer declares a dependency on http:POST /auth/session, a producer's change publishes against the same string, and the two match. File paths never matched: the consumer doesn't know or care which file implements the route.

Each change is then risk-tiered. Touch a contract surface (an extracted route, a .proto or OpenAPI file, anything under a routes/ or controllers/ path) and the change classifies as shared and routes to that surface's consumers. Touch only files you own, and it's owned activity that routes to no one. When classification is uncertain, the bias is deliberate: treat it as shared. A shared surface misclassified as owned is a silent breach; the reverse is just a little noise.

What leaves the machine is coordination metadata only: a summary line, the surface IDs, the risk tier, a hash of the changed-file list. Source and diffs stay local, which is also what makes local verification possible at all, since the ledger's claims can be checked against real code before anything is published.

One thing the hooks deliberately do not do: mint decisions. Early versions auto-created a decision per capture, and it was a category error: it flooded the ledger with saves masquerading as choices. A change is an event; a decision is a durable rule, and it enters the ledger only when the agent deliberately calls propose_decision, or when a human confirms one distilled from Slack. The same durability line, drawn on both sides of the system.

Hooks also fail safe. Capture exits zero on any error (not logged in, repo not connected, API down) because a coordination layer that can break your agent mid-task is a coordination layer you'll uninstall.

The MCP server: one seam, sixteen tools

The hooks are the involuntary layer. The voluntary layer is an MCP server: one process per agent session, started by the vendor's standard MCP config with a LOCKSTEP_VENDOR environment variable as the only vendor-specific bit. It exposes sixteen tools, all thin proxies to the core API: notify, inbox, ack_inbox, query, ask, answer, delegate, complete, propose_decision, ack_decision, register_dependency, decisions, whoowns, consumers, get_product_context, and set_feature_context.

"Thin proxies" is a design position, not an implementation shortcut. The core stays LLM-free: query returns rows, and the agent on the other end does the synthesizing. The intelligence lives in whatever model the developer already chose; the ledger just has to be a fast, structured source of truth. That's also what keeps the server identical across vendors: there is nothing Claude-shaped or Codex-shaped in it. I wrote about why the reasoning layer and the record layer should stay separate in The Feed Is Coming for GitHub; this server is that argument as code.

get_product_context is worth singling out because it's the pull path that complements the push path. The SessionStart briefing is budget-capped: an agent's context window is finite, so the briefing carries only the highest-impact items and says how many more exist. When the agent is about to work on something specific, it calls get_product_context with a scope (a capability ref like feature:checkout, a canonical surface like http:POST /auth/session, or free text) and gets back the full uncapped constraint set for that scope, the list of governed surfaces, and the project's standing principles. Push gives every session cheap ambient awareness; pull gives deep context exactly where the agent is about to dig.

The instructions file, a Claude Code skill in the shipped adapter, is the third surface, teaching the workflow the hooks can't force: read the inbox and binding decisions at session start, query before coding against a shared surface, register_dependency when you build on someone's endpoint, propose_decision when you make a durable call. It's AGENTS.md-shaped, but pointing at live tools instead of static prose.

What's shipped, what's proven, what's not built

Plainly:

Shipped: the Claude Code adapter. Hooks, the MCP server, and the skill, all installed by lockstep init, dogfooded daily.

Proven but not built: Codex CLI and Gemini CLI adapters. The spike verified all three vendors expose the events we need, MCP support, and an instructions file. And the adapter seam was drawn so those are additive: a hook-config shim in each vendor's format plus an AGENTS.md/GEMINI.md variant, pointing at the exact same MCP server and capture binary. But proven feasible is not the same as shipped, and I'd rather you hear that from me than discover it in the docs.

The known gap: hooks only fire while an agent is live. An edit made in a plain editor, or a git commit from someone with no Lockstep install, produces no capture event. That's closed at a different boundary: a CI check on every pull request that reconciles the diff against the ledger, which catches edits made by any means and is the enforcement gate anyway. Real-time capture is the fast path; the PR check is the airtight one.

One more honest note, because it shapes how we think about all of this. The spike's most important finding was not that cross-vendor capture works. It was how easy the capture is. Hooks, MCP, and instruction files are public, documented, low-barrier surfaces on every vendor. Anyone can build this integration in a few weeks. Whatever defensibility Lockstep has will never come from the plumbing described in this post; it comes from the accumulated decision ledger, from being neutral across vendors when every vendor wants lock-in, and from speed. The plumbing is table stakes, which is exactly why it's worth documenting in public.

Frequently asked questions

What integration surfaces do coding agents expose?
All three major CLI agents expose the same trio: lifecycle hooks (commands that fire on events like session start or after a tool call), MCP servers (structured tools the agent can call), and an instructions file the agent reads at startup (CLAUDE.md, AGENTS.md, or GEMINI.md). Names differ per vendor; the shape is the same.
Why use hooks for capture instead of just instructing the agent?
Instructions are conventions: the model can forget them mid-session, and a compaction can drop them entirely. Hooks are configuration the harness executes deterministically on every matching event, whether or not the model remembers. Lockstep uses both: instructions teach the agent to use the tools well, hooks guarantee capture happens regardless.
Does Lockstep's capture read my source code?
Capture runs locally and reads the git diff to extract canonical surface identifiers like http:POST /auth/session. What leaves the machine is coordination metadata: a summary, the surface IDs, a risk tier, and a hash of the changed file list. Source code and diffs never leave the machine.
Which agents does Lockstep auto-capture support today?
Claude Code is the shipped adapter: hooks plus the MCP server plus a skill file, installed by lockstep init. Codex CLI and Gemini CLI expose equivalent hooks, MCP support, and instruction files (we verified feasibility on all three before building), but those adapters are not built yet.

Keep reading