All skills
Skillintermediate

Raw hook outputs

Captured by invoking `capy hook <event>` directly with representative JSON payloads on stdin. This is the authoritative surface — what Claude actually sees when each hook fires.

Claude Code Knowledge Pack7/10/2026

Overview

Raw hook outputs

Captured by invoking capy hook <event> directly with representative JSON payloads on stdin. This is the authoritative surface — what Claude actually sees when each hook fires.

Hook config (from .claude/settings.local.json): all six events dispatch to the same wrapper, bash $CLAUDE_PROJECT_DIR/.claude/scripts/capy.sh hook <type>, which shells out to capy hook <type>.

Hook-output JSON is interpreted by Claude Code:

  • hookSpecificOutput.additionalContext → injected as <system-reminder> text into my context for that turn.
  • hookSpecificOutput.permissionDecision = "deny" + reason → the tool call is denied; I see only the reason.
  • hookSpecificOutput.permissionDecision = "allow" + updatedInput → the tool input is silently rewritten before execution.

SessionStart

Trigger: on session start. Output: 2.3 KB context window protection block (identical to what landed in 01-sessionstart-capy.md).

{
  "hookSpecificOutput": {
    "additionalContext": "<context_window_protection>\
  <priority_instructions>\
    Raw tool output floods your context window. You MUST use capy\
    MCP tools to keep raw data in the sandbox.\
  </priority_instructions>\
\
  <tool_selection_hierarchy>\
    1. GATHER: capy_batch_execute(commands, queries)\
       - Primary tool for research. Runs all commands, auto-indexes, and searches.\
       - ONE call replaces many individual steps.\
    2. FOLLOW-UP: capy_search(queries: [\\"q1\\", \\"q2\\", ...])\
       - Use for all follow-up questions. ONE call, many queries.\
    3. PROCESSING: capy_execute(language, code) | capy_execute_file(path, language, code)\
       - Use for API calls, log analysis, and data processing.\
  </tool_selection_hierarchy>\
\
  <forbidden_actions>\
    - DO NOT use Bash for commands producing >20 lines of output.\
    - DO NOT use Read for analysis (use execute_file). Read IS correct for files you intend to Edit.\
    - DO NOT use WebFetch (use capy_fetch_and_index instead).\
    - Bash is ONLY for git/mkdir/rm/mv/navigation.\
  </forbidden_actions>\
\
  <output_constraints>...[truncated — see 01-sessionstart-capy.md for the full text]",
    "hookEventName": "SessionStart"
  }
}

UserPromptSubmit

Trigger: every user prompt. Output: empty. The hook runs silently — probably used for logging/indexing, not context injection.

(empty)

PreToolUse — matcher Bash|WebFetch|Read|Grep|Agent|Task|mcp__*capy*

This hook has behaviour branches keyed on tool_name. Six observed variants:

PreToolUse — Bash (generic)

Input: {"tool_name":"Bash","tool_input":{"command":"ls","description":"list"}}

{
  "hookSpecificOutput": {
    "additionalContext": "<context_guidance>\
  <tip>\
    This Bash command may produce large output. To stay efficient:\
    - Use capy_batch_execute(commands, queries) for multiple commands\
    - Use capy_execute(language: \\"shell\\", code: \\"...\\") to run in sandbox\
    - Only your final printed summary will enter the context.\
    - Bash is best for: git, mkdir, rm, mv, navigation, and short-output commands only.\
  </tip>\
</context_guidance>",
    "hookEventName": "PreToolUse"
  }
}

PreToolUse — Bash containing curl/wget

Input: {"tool_name":"Bash","tool_input":{"command":"wget https://example.com","description":"fetch"}}

Mutation pathpermissionDecision: "allow" plus updatedInput that replaces the command with a harmless echo that prints a warning. I never see the original command execute; I see only the echo output.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Routed to capy sandbox",
    "updatedInput": {
      "command": "echo \\"capy: curl/wget blocked (stdout flood risk). Use capy_fetch_and_index(url, source) to fetch URLs, or capy_execute(language, code) to run HTTP calls in sandbox. File downloads with -o/--output are allowed.\\""
    }
  }
}

Inline-HTTP variant — when the Bash command contains fetch('http, requests.get(, requests.post(, http.get(, or http.request(, capy emits the same allow/echo pattern with a different message: capy: Inline HTTP blocked. Use capy_execute(language, code) .... (Observed directly this session — my own Bash command that contained the pattern in a test fixture string tripped it.)

PreToolUse — Read

Input: {"tool_name":"Read","tool_input":{"file_path":"/tmp/foo.txt"}}

{
  "hookSpecificOutput": {
    "additionalContext": "<context_guidance>\
  <tip>\
    Read is the right default. Use offset/limit to scope large files.\
    Only reach for capy_execute_file when the file is genuinely large (10k+ lines)\
    AND you want a derived answer (count, stats, extracted pattern), not the content itself.\
    If an Edit will follow, just Read — capy_execute_file beforehand is pure overhead.\
  </tip>\
</context_guidance>",
    "hookEventName": "PreToolUse"
  }
}

PreToolUse — Grep

Input: {"tool_name":"Grep","tool_input":{"pattern":"foo"}}

{
  "hookSpecificOutput": {
    "additionalContext": "<context_guidance>\
  <tip>\
    This operation may flood your context window. To stay efficient:\
    - Use capy_execute(language: \\"shell\\", code: \\"...\\") to run searches in the sandbox.\
    - Only your final printed summary will enter the context.\
  </tip>\
</context_guidance>",
    "hookEventName": "PreToolUse"
  }
}

PreToolUse — WebFetch

Input: {"tool_name":"WebFetch","tool_input":{"url":"https://example.com","prompt":"x"}}

Denial path — the tool is blocked entirely; I see only the reason string.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "capy: WebFetch blocked. Use capy_fetch_and_index(url: \\"https://example.com\\") to fetch this URL in sandbox. Then use capy_search(queries: [...]) to query results."
  }
}

PreToolUse — Agent

Input: {"tool_name":"Agent","tool_input":{"description":"test","prompt":"hi","subagent_type":"general-purpose"}}

Mutation pathpermissionDecision: "allow" plus updatedInput.prompt that appends the full <context_window_protection> block (same as SessionStart) to the end of the sub-agent prompt. This is how capy rules propagate to sub-agents.

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "Routed to capy sandbox",
    "updatedInput": {
      "description": "test",
      "prompt": "hi<context_window_protection>\
  ...[full SessionStart block appended verbatim]...\
</context_window_protection>",
      "subagent_type": "general-purpose"
    }
  }
}

PreToolUse — Task / mcp__capy__*

Input: {"tool_name":"mcp__capy__capy_search","tool_input":{"queries":["test"]}}

Output: empty. The matcher includes these names but the capy binary emits no context for them — they're valid, silently allowed.

(empty)

PostToolUse — matcher all

Trigger: after every tool call (all tools — matcher is ""). Output: empty across every combination I tried (Bash short, Bash large, Read). Side-effect only — likely auto-indexes output into the knowledge base without feeding anything back into my turn.

(empty)

PreCompact

Trigger: before conversation compaction. Output: empty.

(empty)

SessionEnd

Trigger: when the session closes. Output: empty. Claude doesn't have another turn after SessionEnd fires, so there's nothing to inject into anyway — the hook is for side effects (flush WAL, final indexing).

(empty)

Summary — what each hook actually does

EventInjects context?Mutates tool input?Denies?Notes
SessionStartYes (big)The capy routing rules.
UserPromptSubmitNoSilent.
PreToolUse (Bash)Yes (tip)Sometimes (curl/wget/inline HTTP → echo)Generic tip for every Bash; mutation only on HTTP patterns.
PreToolUse (Read)Yes (tip)
PreToolUse (Grep)Yes (tip)
PreToolUse (WebFetch)YesFull block.
PreToolUse (Agent)YesAppends capy rules to sub-agent prompt.
PreToolUse (Task / mcp__capy__*)NoNoNoSilently allowed.
PostToolUseNoSide-effect only (probably auto-index).
PreCompactNoSilent.
SessionEndNoSilent.

Reproduction

printf '{"tool_name":"","tool_input":{...}}' | capy hook pretooluse
printf '{"tool_name":"","tool_input":{...},"tool_response":{...}}' | capy hook posttooluse
printf '{}' | capy hook sessionstart
printf '{}' | capy hook sessionend
printf '{}' | capy hook precompact
printf '{"prompt":"..."}' | capy hook userpromptsubmit

The capy binary is what Claude Code actually invokes per hook event — everything above is ground truth.