The JSON your Claude Code hooks receive on stdin: common and per-event fields

Claude Code Published:

Command hooks read JSON from standard input. The fields every event includes, the extra fields for PreToolUse, Stop and SessionStart, and jq examples for reading them.

Verified on Sep 17, 2026 These tools change quickly. Please also check the latest official documentation.
Contents
  1. Fields present on every event
  2. What each event adds
    1. PreToolUse and PostToolUse
    2. The other events
  3. Reading values with jq
  4. What the JSON doesn't include
  5. Summary

You have the hook configured and your script is running, but nothing tells you which file was just edited or which command is about to execute.

The answer is the JSON that arrives on standard input. Command hooks read JSON from stdin; HTTP hooks receive the same JSON as a POST body with Content-Type: application/json. This article lays out that JSON, split into the fields every event carries and the fields each event adds.

KEY POINT

What you will learn

  • The fields present on every hook event and what each one means
  • The extra fields for PreToolUse, Stop, SessionStart and the other events
  • How to read values with jq, and where to get what the JSON doesn't include

Fields present on every event

Every hook event receives these, in addition to its event-specific fields.

{
  "session_id": "abc123",
  "prompt_id": "550e8400-e29b-41d4-a716-446655440000",
  "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl",
  "cwd": "/home/user/my-project",
  "scratchpad_dir": "/tmp/claude-1000/-home-user-my-project/abc123/scratchpad",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "effort": {
    "level": "medium"
  }
}
FieldDescription
session_idCurrent session identifier
prompt_idUUID identifying the user prompt (absent until first user input; requires v2.1.196+)
transcript_pathPath to the conversation JSON (may lag the current turn)
cwdWorking directory when the hook is invoked
scratchpad_dirThe session's scratchpad directory (absent if unavailable; requires v2.1.257+)
permission_modedefault, plan, acceptEdits, auto, dontAsk or bypassPermissions
effortObject with a level field: low, medium, high, xhigh or max
hook_event_nameName of the event that fired
agent_idSubagent identifier (only in subagent context)
agent_typeAgent name such as Explore (in a subagent, or with --agent)

Because hook_event_name is always present, you can point one script at several events and branch on it inside.

transcript_path is not always current

The documentation notes that transcript_path may lag the current turn. Don't build logic on it that assumes the in-flight exchange has already been written.

What each event adds

PreToolUse and PostToolUse

These are the events you will use most. PreToolUse adds three fields.

{
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test",
    "description": "Run test suite",
    "timeout": 120000,
    "run_in_background": false
  },
  "tool_use_id": "toolu_01ABC123..."
}

The contents of tool_input depend on the tool: Bash carries command, while Edit and Write carry file_path.

PostToolUse has the same shape plus tool_output.

{
  "tool_output": {
    "text": "Test results...",
    "error": null
  }
}

PostToolUseFailure uses the same structure, with the error type in error (for example TimeoutError).

The other events

EventFields addedExample values
UserPromptSubmituser_inputThe prompt text
Stoplast_assistant_message, stop_reasonend_turn
SubagentStopagent_type, agent_id, last_assistant_message, stop_reasoncode-reviewer
SessionStarthow, modelstartup, resume, clear, compact, fork
SessionEndwhy, modelclear, resume, logout, prompt_input_exit, other
PreCompact / PostCompactwhatmanual, auto
Notificationtype, metadatapermission_prompt, idle_prompt, auth_success

Note that the field name changes per event: SessionStart uses how, SessionEnd uses why, and compaction uses what. Check the table rather than guessing. The docs also note that model is not always present.

Most of these values can be filtered at the config level too. For which events match on what, see Every value you can put in a Claude Code hook matcher.

Reading values with jq

The shortest form pipes the command straight into jq. The documentation uses exactly this shape for running Prettier after an edit.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

In a script file, read the whole payload first, then pull fields out of it. This is the documented file-protection script:

#!/usr/bin/env bash
# protect-files.sh

INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# Normalize Windows backslash separators so the patterns below match
FILE_PATH="${FILE_PATH//\\//}"

PROTECTED_PATTERNS=(".env" "package-lock.json" ".git/")

for pattern in "${PROTECTED_PATTERNS[@]}"; do
  if [[ "$FILE_PATH" == *"$pattern"* ]]; then
    echo "Blocked: $FILE_PATH matches protected pattern '$pattern'" >&2
    exit 2
  fi
done

Two details matter here:

  1. // empty prevents jq from returning the literal string null when the hook fires for a tool that has no file_path
  2. Standard input can only be read once. Capture it with INPUT=$(cat) and pull fields from the variable as many times as you need

Exit code 2 blocks the action. For what each exit code means, see the hooks reference.

用語解説

stdin (standard input): the channel that feeds data into a process from outside. Reading it with cat treats it like a file. Once consumed it cannot be read again, which is why you store it in a variable.

What the JSON doesn't include

Path roots and similar context come from environment variables instead. Hook processes inherit the parent environment plus these:

VariableContents
$CLAUDE_PROJECT_DIRProject root where the session started
$CLAUDE_PLUGIN_ROOTPlugin installation directory (for plugin hooks)
$CLAUDE_PLUGIN_DATAThe plugin's persistent data directory
$CLAUDE_EFFORTCurrent effort level (low, medium, high, xhigh, max)
$CLAUDE_CODE_REMOTESet to true in remote web environments
$CLAUDE_CODE_BRIDGE_SESSION_IDRemote Control session ID (v2.1.199+, while connected)

The JSON's cwd is the working directory at invocation time, so it shifts when you work in a subdirectory. To reference a script at a fixed location, use $CLAUDE_PROJECT_DIR.

{
  "type": "command",
  "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-rm-rf.sh"
}

For a concrete lint-and-format setup built on this, see Run lint and format automatically after every edit.

Summary

  • Command hooks read JSON from stdin; HTTP hooks get the same JSON as a POST body
  • Every event includes session_id, transcript_path, cwd, permission_mode, hook_event_name and effort
  • Tool events add tool_name, tool_input and tool_use_id, and PostToolUse adds tool_output
  • Field names differ per event: how for SessionStart, why for SessionEnd, what for compaction
  • In scripts, capture with INPUT=$(cat) and read fields with jq -r '... // empty'
  • The project root is not in the JSON; read $CLAUDE_PROJECT_DIR instead

FAQ

How does a hook script receive its input?
Command hooks receive JSON on standard input. HTTP hooks receive the same JSON as the body of a POST request with Content-Type application/json.
Where is the path of the file that was edited?
Inside tool_input on PreToolUse and PostToolUse. For Edit or Write, read it with .tool_input.file_path.
Are there fields available on every event?
Yes. session_id, transcript_path, cwd, permission_mode and hook_event_name are included on every hook event.
How do I get the project root?
The JSON's cwd is the working directory at the moment the hook runs. For the project root where the session started, use the $CLAUDE_PROJECT_DIR environment variable.

Primary sources

This article was drafted by AI from official documentation and reviewed by the site operator before publishing. Found a mistake? Let us know via the contact page.