PostToolBatch
Runs once after every tool call in a batch has resolved, before Claude Code sends the next request to the model. PostToolUse fires once per tool, which means it fires concurrently when Claude makes parallel tool calls. PostToolBatch fires exactly once with the full batch, so it is the right place to inject context that depends on the set of tools that ran rather than on any single tool. There is no matcher for this event.
PostToolBatch input
In addition to the common input fields, PostToolBatch hooks receive tool_calls, an array describing every tool call in the batch:
{
"session_id": "abc123",
"transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PostToolBatch",
"tool_calls": [
{
"tool_name": "Read",
"tool_input": {"file_path": "/.../ledger/accounts.py"},
"tool_use_id": "toolu_01...",
"tool_response": " 1\tfrom __future__ import annotations\n 2\t..."
},
{
"tool_name": "Read",
"tool_input": {"file_path": "/.../ledger/transactions.py"},
"tool_use_id": "toolu_02...",
"tool_response": " 1\tfrom __future__ import annotations\n 2\t..."
}
]
}tool_response contains the same content the model receives in the corresponding tool_result block. The value is a serialized string or content-block array, exactly as the tool emitted it. For Read, that means line-number-prefixed text rather than raw file contents. Responses can be large, so parse only the fields you need.
The tool_response shape differs from PostToolUse's. PostToolUse passes the tool's structured Output object, such as \{filePath: "...", type: "create"\} for Write; PostToolBatch passes the serialized tool_result content the model sees.
PostToolBatch decision control
PostToolBatch hooks can inject context for Claude. In addition to the JSON output fields available to all hooks, your hook script can return these event-specific fields:
| Field | Description |
|---|---|
additionalContext | Context string injected once before the next model call. See Add context for Claude for delivery details, what to put in it, and how resumed sessions handle past values |
{
"hookSpecificOutput": {
"hookEventName": "PostToolBatch",
"additionalContext": "These files are part of the ledger module. Run pytest before marking the task complete."
}
}Returning decision: "block" or continue: false stops the agentic loop before the next model call. The blocking message comes from the JSON reason or stopReason, or from stderr on exit 2. You see it as a warning in the transcript, and it stays in the conversation, so Claude sees it when the conversation continues.