Claude Code Adapter
Native event names: PascalCase. Reference doc: https://code.claude.com/docs/en/hooks.
Stdin → unified
Claude hook_event_name | Unified |
|---|---|
SessionStart | SessionStart (preserves source) |
PreToolUse | PreToolUse (tool: tool_name, tool_input passthrough) |
PostToolUse | PostToolUse (adds tool_response) |
UserPromptSubmit | UserPromptSubmit (prompt passthrough) |
Stop | Stop (preserves stop_hook_active) |
Other Claude events (SessionEnd, SubagentStop, PreCompact, Notification) are not part of the universal-five and the adapter throws on them.
Unified → stdout
The adapter emits Claude's hookSpecificOutput shape:
| Unified field | Claude wire mapping |
|---|---|
decision: "allow" on PreToolUse | hookSpecificOutput.permissionDecision = "allow" |
decision: "deny" on PreToolUse | hookSpecificOutput.permissionDecision = "deny" + exit 2 |
decision: "ask" on PreToolUse | hookSpecificOutput.permissionDecision = "ask" |
reason on PreToolUse | hookSpecificOutput.permissionDecisionReason |
additional_context on PreToolUse | hookSpecificOutput.additionalContext (appears next to the tool result as a system reminder) |
modified_input on PreToolUse | hookSpecificOutput.updatedInput (mutates the tool's input before execution) |
decision: "deny" on others | top-level decision: "block" + reason + exit 2 |
additional_context on SessionStart / UserPromptSubmit / PostToolUse | hookSpecificOutput.additionalContext |
Dropped fields
The following fields have no Claude equivalent and are dropped with a stderr warning:
user_message: Claude has no human-facing message channel.modified_inputon events other thanPreToolUse: Claude only accepts input mutation at the pre-tool stage.
Round-trip example
Stdin (sent by Claude):
jsonc
{
"hook_event_name": "PreToolUse",
"session_id": "abc-123",
"transcript_path": "/Users/x/.claude/projects/foo/abc-123.jsonl",
"cwd": "/Users/x/proj",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/foo"
}
}Handler:
ts
defineHook({
PreToolUse: (event) => {
if (event.tool === "Bash" && /rm -rf/.test(String(event.tool_input.command))) {
return { decision: "deny", reason: "rm -rf is blocked by policy" };
}
return { decision: "allow" };
},
});Stdout (consumed by Claude):
jsonc
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf is blocked by policy"
}
}exit code: 2
Generated config
bunx @pivanov/agent-hooks-bridge install ./.hooks/format.ts writes .claude/settings.json:
jsonc
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "./.hooks/format.ts --host claude"
}
]
}
],
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "./.hooks/format.ts --host claude"
}
]
}
],
"PostToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "./.hooks/format.ts --host claude"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "./.hooks/format.ts --host claude"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./.hooks/format.ts --host claude"
}
]
}
]
}
}matcher is omitted for non-tool events.