Skip to content

Claude Code Adapter

Native event names: PascalCase. Reference doc: https://code.claude.com/docs/en/hooks.

Stdin → unified

Claude hook_event_nameUnified
SessionStartSessionStart (preserves source)
PreToolUsePreToolUse (tool: tool_name, tool_input passthrough)
PostToolUsePostToolUse (adds tool_response)
UserPromptSubmitUserPromptSubmit (prompt passthrough)
StopStop (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 fieldClaude wire mapping
decision: "allow" on PreToolUsehookSpecificOutput.permissionDecision = "allow"
decision: "deny" on PreToolUsehookSpecificOutput.permissionDecision = "deny" + exit 2
decision: "ask" on PreToolUsehookSpecificOutput.permissionDecision = "ask"
reason on PreToolUsehookSpecificOutput.permissionDecisionReason
additional_context on PreToolUsehookSpecificOutput.additionalContext (appears next to the tool result as a system reminder)
modified_input on PreToolUsehookSpecificOutput.updatedInput (mutates the tool's input before execution)
decision: "deny" on otherstop-level decision: "block" + reason + exit 2
additional_context on SessionStart / UserPromptSubmit / PostToolUsehookSpecificOutput.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_input on events other than PreToolUse: 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.

Not affiliated with Anthropic, Anysphere, or OpenAI. Supported by LogicStar AI.