Hooks Reference
This document provides the technical specification for copilot-shell hooks, including JSON schemas and API details for all 13 wired events.
Global Hook Mechanics
- Communication:
stdinreceives input (JSON),stdoutoutputs results (JSON),stderroutputs logs - Exit codes:
0: Success;stdoutis parsed as JSON2: System block; operation is aborted;stderrused as rejection reasonOther: Warning; non-fatal failure; CLI continues
- Golden Rule: Scripts must not output anything to
stdoutother than JSON
Base Input Schema
All hooks receive the following common fields via stdin:
{
"session_id": "string",
"run_id": "string | undefined",
"transcript_path": "string",
"cwd": "string",
"hook_event_name": "string",
"timestamp": "string (ISO 8601)"
}
| Field | Type | Description |
|---|---|---|
session_id | string | Unique session identifier |
run_id | string | undefined | Current agent run identifier (format: {sessionId}########{counter}) |
transcript_path | string | Path to the session JSONL transcript file |
cwd | string | Current working directory |
hook_event_name | string | Name of the event that triggered this hook |
timestamp | string | Event trigger time (ISO 8601) |
Common Output Fields
Most hooks support the following fields in the stdout JSON:
| Field | Type | Description |
|---|---|---|
systemMessage | string | Notification message shown to the user |
suppressOutput | boolean | When true, hides internal metadata |
continue | boolean | When false, immediately stops the agent loop |
stopReason | string | Reason shown to user when stopping |
decision | string | "allow" / "deny" / "ask" / "approve" |
reason | string | Feedback message when denying/blocking |
hookSpecificOutput | object | Event-specific output fields |
Tool Hooks
PreToolUse
Triggered before tool execution. Used for argument validation, security checks, and parameter rewriting.
Input fields:
| Field | Type | Description |
|---|---|---|
tool_use_id | string | Unique tool call identifier |
tool_name | string | Name of the tool being called |
tool_input | object | Original parameters generated by the model |
mcp_context | object | Optional MCP tool metadata |
original_request_name | string | Original name in tail calls |
Output fields:
decision: Set to"deny"to block tool executionsystemMessage: Displayed as a standalone notification with hook name labelreason: Required when denying; sent to agent as tool errorhookSpecificOutput.tool_input: Merge-overwrites model parameterscontinue: Set tofalseto terminate the entire agent loop
PostToolUse
Triggered after tool execution. Used for result auditing, context injection, or hiding sensitive output.
Input fields:
| Field | Type | Description |
|---|---|---|
tool_use_id | string | Unique tool call identifier |
tool_name | string | Tool name |
tool_input | object | Original parameters |
tool_response | object | Execution result |
mcp_context | object | MCP metadata |
Output fields:
decision: Set to"deny"to hide the real outputreason: When denied, replaces the tool result sent to the modelhookSpecificOutput.additionalContext: Appended to tool resulthookSpecificOutput.tailToolCallRequest:{ name, args }to immediately execute another toolcontinue: Set tofalseto terminate the agent loop
PostToolUseFailure
Triggered after tool execution failure. Used for error recovery and sandbox bypass.
Input fields:
| Field | Type | Description |
|---|---|---|
tool_use_id | string | Unique tool call identifier |
tool_name | string | Tool name |
tool_input | object | Original parameters |
error | string | Error description |
error_type | string | Error type (e.g., "timeout", "permission") |
is_interrupt | boolean | Whether caused by user interrupt |
Output fields:
hookSpecificOutput.additionalContext: Context to help the agent recoverhookSpecificOutput.sandbox_bypass_request:{ original_command, reason }to request sandbox bypass
Agent Hooks
UserPromptSubmit
Triggered after user submits a prompt, before agent begins planning.
Input fields:
prompt: The raw text submitted by the user
Output fields:
hookSpecificOutput.additionalContext: Text appended to the current turn's promptdecision: Set to"deny"to block the turn and discard the messagecontinue: Set tofalseto block the turn but keep the messagereason: Required when denying or stopping
Stop
Triggered when the agent is about to stop. Used for response validation and auto-retry.
Input fields:
stop_hook_active: Whether already in a retry sequencelast_assistant_message: The final text generated by the agent
Output fields:
decision: Set to"deny"to reject the response and force retryreason: When denied, sent as a new prompt to the agentcontinue: Set tofalseto stop the sessionstopReason: Shown to user when stopping
Model Hooks
BeforeModel
Triggered before sending an LLM request. Uses a stable SDK-agnostic format via the Hook Translator.
Input fields:
llm_request: Containsmodel,messages,config, and optionaltoolConfig
Output fields:
hookSpecificOutput.llm_request: Overwrites partial request fields (e.g., swap model, adjust temperature)hookSpecificOutput.llm_response: Synthetic response; when provided, skips the LLM calldecision: Set to"deny"to block this model request
BeforeToolSelection
Triggered before the LLM decides which tools to call. Used to filter available tool set.
Input fields:
llm_request: Same format asBeforeModel
Output fields:
hookSpecificOutput.toolConfig.mode:"AUTO"/"ANY"/"NONE""NONE": Disable all tools (highest priority)"ANY": Force at least one tool call
hookSpecificOutput.toolConfig.allowedFunctionNames: Tool allowlist
Merge strategy: Multiple hooks' allowlists are unioned.
AfterModel
Triggered after receiving an LLM response. Used for observation, logging, or stop signals.
Input fields:
llm_request: Original requestllm_response: Model response
Output fields:
hookSpecificOutput.llm_response: Replaces stored historydecision: Set to"deny"to discard the response from historycontinue: Set tofalseto stop after the current turn
Lifecycle & System Hooks
SessionStart
Triggered after app startup, session resume, or /clear command.
Input fields: source ("startup" / "resume" / "clear" / "compact")
Output fields:
hookSpecificOutput.additionalContext: Injected as first-turn contentsystemMessage: Displayed at session start- Advisory only:
continueanddecisionare ignored
SessionEnd
Triggered when the CLI exits or session is cleared.
Input fields: reason ("clear" / "logout" / "prompt_input_exit" / "other")
Output fields: systemMessage (displayed on close)
Notification
Triggered when the CLI issues a system notification (e.g., tool permission reminders).
Input fields:
notification_type: Notification typemessage: Notification summarydetails: Notification metadata
Advisory only; cannot block notifications.
PreCompact
Triggered before the CLI compresses history to save tokens.
Input fields: trigger ("auto" / "manual")
Advisory only; cannot block or modify the compression process.
PermissionRequest
Triggered when a permission dialog is displayed.
Input fields:
permission_mode: Current permission modetool_name: Tool nametool_input: Tool parameterspermission_suggestions: Suggestion list
Output fields:
hookSpecificOutput.decision:{ behavior: "allow"\|"deny", updatedInput?, message?, interrupt? }
Stable Model API
copilot-shell uses a Hook Translator layer to decouple hook scripts from the underlying SDK.
LLMRequest
{
"model": "string",
"messages": [
{ "role": "user | model | system", "content": "string" }
],
"config": {
"temperature": 0.7,
"maxOutputTokens": 8192,
"topP": 0.95,
"topK": 40
},
"toolConfig": {
"mode": "AUTO | ANY | NONE",
"allowedFunctionNames": ["read_file", "write_file"]
}
}
LLMResponse
{
"text": "string",
"candidates": [
{
"content": { "role": "model", "parts": ["text"] },
"finishReason": "STOP | MAX_TOKENS | SAFETY | OTHER",
"index": 0
}
],
"usageMetadata": {
"promptTokenCount": 100,
"candidatesTokenCount": 200,
"totalTokenCount": 300
}
}
Mode Priority (Multi-Hook Aggregation)
NONEalways takes priority (most restrictive)ANY>AUTOallowedFunctionNamesare unioned (sorted for determinism)
Hooks Reference
This document provides the technical specification for copilot-shell hooks, including JSON schemas and API details for 13 currently wired hook events.
Global hook mechanics
- Communication:
stdinfor Input (JSON),stdoutfor Output (JSON), andstderrfor logs and feedback. - Exit codes:
0: Success.stdoutis parsed as JSON. Preferred for all logic.2: System Block. The action is blocked;stderris used as the rejection reason.Other: Warning. A non-fatal failure occurred; the CLI continues with a warning.
- Silence is Mandatory: Your script must not print any plain text to
stdoutother than the final JSON.
Base input schema
All hooks receive these common fields via stdin:
{
"session_id": "string",
"run_id": "string | undefined",
"transcript_path": "string",
"cwd": "string",
"hook_event_name": "string",
"timestamp": "string (ISO 8601)"
}
| Field | Type | Description |
|---|---|---|
session_id | string | Unique identifier for the CLI session (1 session = N runs). |
run_id | string | undefined | Unique identifier for the current agent run (1 run = 1 user prompt → complete response). Format: {sessionId}########{counter}. Undefined for session-level events (SessionStart, SessionEnd) and for UserPromptSubmit (which fires before the run begins). |
transcript_path | string | Path to the session's JSONL transcript file. |
cwd | string | Current working directory. |
hook_event_name | string | The event that triggered this hook. |
timestamp | string | ISO 8601 timestamp of when the event fired. |
Common output fields
Most hooks support these fields in their stdout JSON:
| Field | Type | Description |
|---|---|---|
systemMessage | string | Shown to the user as a per-hook notification box labeled with the hook name, independent of the tool confirmation dialog. |
suppressOutput | boolean | If true, hides internal hook metadata from logs/telemetry. |
continue | boolean | If false, stops the entire agent loop immediately. |
stopReason | string | Displayed to the user when continue is false. |
decision | string | "allow", "deny" (alias "block"), "ask", or "approve". |
reason | string | The feedback/error message used for "deny"/"block" decisions and stop-like flows. For user-visible allow/approve/ask messaging, prefer systemMessage; if omitted, reason is used as the fallback text for the notification. |
hookSpecificOutput | object | Event-specific output fields (see individual event sections). |
Tool hooks
PreToolUse
Fires before a tool is invoked. Used for argument validation, security checks, and parameter rewriting.
- Input Fields:
tool_use_id: (string) Optional unique identifier for the tool use. It is the same value exposed toPostToolUse, so hooks can correlate the before/after events for one tool call.tool_name: (string) The name of the tool being called.tool_input: (object) The raw arguments generated by the model.mcp_context: (object) Optional metadata for MCP-based tools.original_request_name: (string) The original name if this is a tail call.
- Relevant Output Fields:
decision: Set to"deny"(or"block") to prevent tool execution.systemMessage: Any informational or warning text you want the user to see. It is rendered as a separate, per-hook notification box (labeled with the hook name) above the tool confirmation dialog, regardless of the final decision. When the overall outcome isblock/deny, per-hook boxes whose owndecisionis not blocking are dimmed so they do not visually conflict with the denied outcome.reason: Required if denied. Sent to the agent as a tool error. Also used as the fallback notification text whensystemMessageis omitted.hookSpecificOutput.tool_input: An object that merges with and overrides the model's arguments before execution.continue: Set tofalseto kill the entire agent loop.
- Ask-dialog note: When
decisionis"ask", the tool confirmation dialog always uses a fixed prompt —A hook requires your confirmation to proceed.— instead of the hook'ssystemMessage. The hook'ssystemMessageis still shown as a separate notification box above the dialog, so users see both the reason and the confirmation choice. - Exit Code 2 (Block Tool): Prevents execution. Uses
stderras reason.
PostToolUse
Fires after a tool executes. Used for result auditing, context injection, or hiding sensitive output from the agent.
- Input Fields:
tool_use_id: (string) Optional unique identifier for the tool use.tool_name: (string)tool_input: (object) The original arguments.tool_response: (object) The result.mcp_context: (object) Optional MCP metadata.original_request_name: (string)
- Relevant Output Fields:
decision: Set to"deny"to hide the real tool output from the agent.reason: Required if denied. Replaces the tool result sent to model.hookSpecificOutput.additionalContext: Appended to the tool result.hookSpecificOutput.tailToolCallRequest: ({ name, args }) Execute another tool immediately; its result replaces the original response.continue: Set tofalseto kill the agent loop.
PostToolUseFailure
Fires when a tool execution fails. Used for error recovery and sandbox bypass.
- Input Fields:
tool_use_id: (string) Unique identifier for the tool use.tool_name: (string)tool_input: (object)error: (string) Error message describing the failure.error_type: (string) Type of error (e.g.,"timeout","permission").is_interrupt: (boolean) Whether failure was caused by user interruption.
- Relevant Output Fields:
hookSpecificOutput.additionalContext: Context to help the agent recover.hookSpecificOutput.sandbox_bypass_request: ({ original_command, reason }) Request to bypass sandbox and re-run the original command.
Agent hooks
UserPromptSubmit
Fires after a user submits a prompt, before the agent begins planning. Used for prompt validation or injecting dynamic context.
- Input Fields:
prompt: (string) The original text submitted by the user.
- Relevant Output Fields:
hookSpecificOutput.additionalContext: Text appended to the prompt for this turn only.decision: Set to"deny"to block the turn and discard the message.continue: Set tofalseto block the turn but save the message.reason: Required if denied or stopped.
Stop
Fires when the agent is about to stop. Used for response validation and automatic retries.
- Input Fields:
stop_hook_active: (boolean) Indicates if already running as part of a retry sequence.last_assistant_message: (string) The final text generated by the agent.
- Relevant Output Fields:
decision: Set to"deny"to reject the response and force a retry.reason: Required if denied. Sent to the agent as a new prompt.continue: Set tofalseto stop the session.stopReason: Displayed to the user when stopping.
Model hooks
BeforeModel
Fires before sending a request to the LLM. Operates on a stable, SDK-agnostic request format via the Hook Translator.
- Input Fields:
llm_request: (object) Containsmodel,messages,config, and optionaltoolConfig.
- Relevant Output Fields:
hookSpecificOutput.llm_request: An object that overrides parts of the outgoing request (e.g., changing models or temperature).hookSpecificOutput.llm_response: A Synthetic Response object. If provided, the CLI skips the LLM call entirely and uses this as the response.decision: Set to"deny"to block this model attempt. Without a synthetic response, the request path returns an empty stream.
- Important Behavior Note: If blocked without a synthetic response, the empty stream may be handled by stream validation/retry logic rather than immediately terminating the turn.
- Exit Code 2 (Block Turn): Treated as a blocking decision (
deny) for this model attempt.
BeforeToolSelection
Fires before the LLM decides which tools to call. Used to filter the available toolset or force specific tool modes.
- Input Fields:
llm_request: (object) Same format asBeforeModel.
- Relevant Output Fields:
hookSpecificOutput.toolConfig.mode: ("AUTO" \| "ANY" \| "NONE")"NONE": Disables all tools (wins over other hooks)."ANY": Forces at least one tool call.
hookSpecificOutput.toolConfig.allowedFunctionNames: (string[]) Whitelist of tool names.
- Union Strategy: Multiple hooks' whitelists are combined.
- Limitations: Does not support
decision,continue, orsystemMessage.
AfterModel
Fires after receiving an LLM response. Used for real-time observation, logging, or stop signal.
- Input Fields:
llm_request: (object) The original request.llm_response: (object) The model's response.
- Relevant Output Fields:
hookSpecificOutput.llm_response: An object that replaces the stored history entry for this turn. Note: streaming text already rendered to the terminal cannot be reverted; only the in-memory history (used for future context) is updated.decision: Set to"deny"to discard the response from history and block the turn (prevents tool calls from executing).continue: Set tofalseto stop the agent loop after the current turn.
Lifecycle & system hooks
SessionStart
Fires on application startup, resuming a session, or after a /clear command.
- Input fields:
source: ("startup" \| "resume" \| "clear" \| "compact")
- Relevant output fields:
hookSpecificOutput.additionalContext: Injected as the first turn.systemMessage: Shown at the start of the session.
- Advisory only:
continueanddecisionfields are ignored.
SessionEnd
Fires when the CLI exits or a session is cleared.
- Input Fields:
reason: ("clear" | "logout" | "prompt_input_exit" | "bypass_permissions_disabled" | "other")
- Relevant Output Fields:
systemMessage: Displayed to the user during shutdown.
- Execution Timing: SessionEnd is executed during shutdown and is awaited in normal cleanup paths. It remains non-blocking in intent (hook failures do not prevent process exit).
Notification
Fires when the CLI emits a system alert (e.g., Tool Permissions).
- Input Fields:
notification_type: ("ToolPermission")message: Summary of the alert.details: JSON object with alert-specific metadata.
- Relevant Output Fields:
systemMessage: Displayed alongside the system alert.
- Observability Only: Cannot block alerts.
PreCompact
Fires before the CLI summarizes history to save tokens.
- Input Fields:
trigger: ("auto" \| "manual")custom_instructions: (string) Optional custom instructions.
- Relevant Output Fields:
systemMessage: Displayed before compression.
- Advisory Only: Cannot block or modify the compression process.
PermissionRequest
Fires when a permission dialog is displayed.
- Input Fields:
permission_mode: (string) Current permission mode.tool_name: (string)tool_input: (object)permission_suggestions: Array of{ type, tool? }suggestions.
- Relevant Output Fields:
hookSpecificOutput.decision:{ behavior: "allow"|"deny", updatedInput?, updatedPermissions?, message?, interrupt? }
Stable Model API
copilot-shell uses a Hook Translator layer to decouple hook scripts from
the underlying SDK types (@google/genai). This ensures hooks don't break
across SDK updates.
LLMRequest
{
"model": "string",
"messages": [
{
"role": "user | model | system",
"content": "string (text-only, non-text parts are filtered)"
}
],
"config": {
"temperature": 0.7,
"maxOutputTokens": 8192,
"topP": 0.95,
"topK": 40
},
"toolConfig": {
"mode": "AUTO | ANY | NONE",
"allowedFunctionNames": ["read_file", "write_file"]
}
}
LLMResponse
{
"text": "string (convenience field, first candidate text)",
"candidates": [
{
"content": {
"role": "model",
"parts": ["text part 1", "text part 2"]
},
"finishReason": "STOP | MAX_TOKENS | SAFETY | RECITATION | OTHER",
"index": 0,
"safetyRatings": [
{ "category": "string", "probability": "string", "blocked": false }
]
}
],
"usageMetadata": {
"promptTokenCount": 100,
"candidatesTokenCount": 200,
"totalTokenCount": 300
}
}
HookToolConfig
{
"mode": "AUTO | ANY | NONE",
"allowedFunctionNames": ["tool_name_1", "tool_name_2"]
}
Mode priority (multi-hook aggregation)
When multiple hooks return different modes, they are aggregated:
NONEalways wins (most restrictive)ANY>AUTOallowedFunctionNamesare unioned across all hooks (sorted for deterministic behavior)