Agents and adapters
agent-blackbox records and gates one canonical event stream. Claude Code's hook events (SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Stop, ...) with Claude Code's tool names (Bash, Read, Write, Edit, WebFetch, mcp__server__tool) are that format. The policy, the vault and the ledger know nothing else.
An adapter (src/adapters/<agent>.js) is the only code that knows one agent's own hook format. It does three things:
| Method | Does |
|---|---|
decode(native) | turns the agent's hook payload into a canonical event, naming tools the way the policy expects (run_shell_command becomes Bash). Returns null for events nothing is recorded for. |
encode(reply, native, opts) | turns the recorder's neutral verdict (permission: 'ask' | 'deny' | null, the human's reason, the model's uninformative message, a notice) into what the agent reads on stdout and by exit code. |
failClosed(event, reason, native) | what to print when the recorder is down and failMode is closed. |
capabilities states, per agent, what its hooks can do. It is documentation the code can check, not a promise made on the agent's behalf:
preTool: a hook can stop a tool call before it runs. Without it the agent is recorded but not enforced.ask: a hook can hand the decision to the human. Without it, anaskverdict becomes a block (or is let through with a notice, whenaskFallbackisallowin~/.blackbox/config.json).postTool: tool results reach a hook. Without it the policy cannot learn what a session has read, so the trifecta rules cannot fire.prompt,session: prompts and session start/end are recorded.
Events recorded for an agent other than Claude Code carry agent (codex, cursor, gemini); Claude Code's carry none, as before.
The hook script picks its adapter with --agent <id> (default claude). Adapters load lazily and use no dependencies.
What each agent can enforce
| Agent | Install | Block before a tool runs | Ask the human | Sees tool results | Notes |
|---|---|---|---|---|---|
| Claude Code | blackbox install (or the plugin) | yes | yes | yes | 14 lifecycle events, OpenTelemetry too |
| OpenAI Codex CLI | blackbox install --agent codex | yes, for Bash, apply_patch and MCP calls | no: an ask becomes a block | yes | see below |
| Gemini CLI | blackbox install --agent gemini | yes, for every tool | no: an ask becomes a block | yes | see below |
| Cursor | blackbox install --agent cursor | yes, for shell and MCP calls | yes | shell, MCP and file reads (with content) | file edits are recorded after the fact; see below |
Codex CLI
Hooks live in ~/.codex/hooks.json (or $CODEX_HOME). The adapter registers SessionStart, UserPromptSubmit, PreToolUse, PostToolUse and Stop. Codex's payloads follow Claude Code's, so the adapter only renames what differs: an apply_patch call becomes an Edit of every file in the patch (so the memory-write and hook-tamper rules see all of them), and web_search becomes WebSearch.
Limits to know before relying on it:
PreToolUsedoes not see every tool. It covers Bash,apply_patchand MCP calls. Codex's other built-ins, such as web search, are not gated, and what they return is not seen by the policy.- Codex cannot ask you. Where Claude Code would show a permission prompt, Codex gets a block (the model sees only the uninformative message, you see the reason). Set
"askFallback": "allow"in~/.blackbox/config.jsonto let those calls run with a notice instead. - Hooks are experimental in Codex and may need enabling in
~/.codex/config.toml; the installer says so. ThePermissionRequestevent is not used. - The payload shapes above were taken from third-party write-ups of the Codex hooks, not from OpenAI's own documentation (not reachable when this was written). Check them against your Codex version; a payload the adapter cannot read is skipped, never a reason to stop the agent.
Cursor
Hooks live in ~/.cursor/hooks.json, one command per event name. The adapter registers beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, beforeReadFile, afterFileEdit, beforeSubmitPrompt and stop, and maps them to the canonical events (beforeShellExecution is a PreToolUse of Bash, an MCP call is mcp__<server>__<tool>, and so on). Cursor names the MCP tool but not always the server, so the server is taken from the payload's server name, else the URL's host, else the command's name.
Limits to know before relying on it:
- File edits are not gated. Cursor's edit hook runs after the edit (
afterFileEdit), so the memory-write rule and the hook-tamper rule on an edit are seen and recorded, but cannot stop the write. Shell commands, includingsed -iand redirects, are gated as usual. - File reads are never blocked. The read hook carries the file's content, so the session learns what it read (private data, injected text), and the trifecta rule fires later on the egress.
- Other Cursor hook points are not used. Newer events (a generic tool hook, session start and end, subagents) are not registered until they have been checked against a real Cursor.
- The reply fields are written in both
snake_caseandcamelCase(user_messageanduserMessage), because the documentation and the type definitions I could reach disagree. As with Codex, the payload shapes come from third-party examples; the official page was not reachable. Restart Cursor after installing.
Gemini CLI
Hooks live in ~/.gemini/settings.json under hooks. The adapter registers SessionStart, SessionEnd, BeforeAgent (the prompt), AfterAgent, BeforeTool, AfterTool and Notification, and renames the tools: run_shell_command is Bash, read_file and read_many_files are Read, write_file is Write, replace is Edit, web_fetch is WebFetch, google_web_search is WebSearch, and MCP tools (named by their mcp_context) are mcp__<server>__<tool>. save_memory, which appends to the GEMINI.md later sessions load as instructions, is seen as a write to that file, so the memory-write rule covers it. Gemini's web_fetch takes a prompt that holds the URLs; every URL in it is checked.
Limits to know before relying on it:
- Gemini cannot ask you. An
askbecomes a block ("askFallback": "allow"lets it run with a notice). Denials usedecision: "deny"; the model sees only the uninformative reason and you see the detail. - Hooks may need enabling in your Gemini settings, and apply to new sessions.
- Gemini's model-level hooks (
BeforeModel,AfterModel,BeforeToolSelection) are not used; the recorder sees tool calls, prompts and turns, not model traffic, and there is no OpenTelemetry stream like Claude Code's. - The event names, tool names and reply shape follow Gemini CLI's hooks reference in its repository. The tool parameter names (
file_path,prompt,paths) are from memory of Gemini's tools and have not been run against a real Gemini; check them with your version.
Adding an agent
src/adapters/<id>.jsexporting anAdapter(seesrc/types.d.ts), and its id insrc/adapters/index.js.- Map the agent's events and tool names onto the canonical ones. Keep fields the policy reads (
command,file_path,url,tool_response) under Claude Code's names. - State only the capabilities you verified against the agent's own documentation, and say where it cannot block.
- Test it with
test/adapter-harness.js, which runs the real hook script against a real recorder.