| description | VS Code agent hooks (Preview) — lifecycle events, input/output schemas, and gotchas based on official documentation |
|---|---|
| applyTo | .github/hooks/**, .github/agents/*.agent.md, .claude/settings*.json |
Reference for writing and reviewing hook scripts and agent-scoped hook configurations. Source: https://code.visualstudio.com/docs/copilot/customization/hooks
Eight hook events fire at specific points during an agent session:
| Event | Fires when | Typical use |
|---|---|---|
| SessionStart | First prompt of a new session | Initialize resources, validate project state |
| UserPromptSubmit | User submits a prompt | Audit requests, inject system context |
| PreToolUse | Before agent invokes any tool | Block dangerous ops, require approval, modify input |
| PostToolUse | After tool completes successfully | Run formatters, log results |
| PreCompact | Before context is compacted | Export state before truncation |
| SubagentStart | Subagent is spawned | Track nested agent usage, initialize resources |
| SubagentStop | Subagent completes | Aggregate results, cleanup resources |
| Stop | Agent session ends | Generate reports, cleanup, send notifications |
Every hook receives JSON via stdin with these fields:
{
"timestamp": "2026-02-09T10:30:00.000Z",
"cwd": "/path/to/workspace",
"sessionId": "session-identifier",
"hookEventName": "PreToolUse",
"transcript_path": "/path/to/transcript.json"
}All input field names use snake_case.
source(string): How the session started. Currently always"new".
prompt(string): The text the user submitted.- Output: uses common output format only (no
hookSpecificOutput).
tool_name(string): The tool being invoked (e.g.,"editFiles","create_file").tool_input(object): The tool's input arguments.tool_use_id(string): Unique identifier for the tool invocation.- PostToolUse additionally receives
tool_response(string).
VS Code tool names differ from Claude Code (e.g., VS Code uses create_file / replace_string_in_file, not Write / Edit).
trigger(string): How compaction was triggered ("auto"when conversation exceeds prompt budget).
agent_id(string): Unique identifier for the subagent instance.agent_type(string): The agent name (e.g.,"Plan"for built-in, or custom agent names like"Code Review Agent").
agent_id(string): Unique identifier for the subagent instance.agent_type(string): The agent name.stop_hook_active(boolean):truewhen the subagent is already continuing from a previous stop hook. Check this to prevent infinite loops.
stop_hook_active(boolean):truewhen the agent is already continuing from a previous stop hook. Check this to prevent infinite loops.- Stop does NOT provide
agent_idoragent_type— unless it is an agent-scoped Stop hook (see below).
When a Stop hook is defined in a custom agent's frontmatter, it is treated as SubagentStop when that agent runs as a subagent. This means agent-scoped Stop hooks do receive agent_id and agent_type in the input, and must use the SubagentStop output format.
{
"continue": true,
"stopReason": "Security policy violation",
"systemMessage": "Warning displayed to user"
}continue: falsestops the entire agent session (drastic).systemMessagedisplays a warning regardless of other decisions.
| Code | Behavior |
|---|---|
| 0 | Success — parse stdout as JSON |
| 2 | Blocking error — stop processing, show stderr to model |
| Other | Non-blocking warning — show warning, continue processing |
Uses hookSpecificOutput wrapper:
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"decision": "block",
"reason": "Run the test suite before finishing"
}
}Uses top-level decision / reason (no hookSpecificOutput wrapper):
{
"decision": "block",
"reason": "Verify subagent results before completing"
}Inject context via hookSpecificOutput:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Project: my-app v2.1.0 | Branch: main"
}
}Control tool execution via hookSpecificOutput:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Destructive command blocked",
"updatedInput": {},
"additionalContext": "Extra context for the model"
}
}permissionDecision values: "allow", "deny", "ask". When multiple hooks fire, the most restrictive wins (deny > ask > allow).
{
"decision": "block",
"reason": "Post-processing validation failed",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "Lint errors found in edited file"
}
}| Scope | Path |
|---|---|
| Workspace | .github/hooks/*.json |
| Workspace (Claude format) | .claude/settings.json, .claude/settings.local.json |
| User | ~/.copilot/hooks, ~/.claude/settings.json |
| Custom agent | hooks field in .agent.md frontmatter |
| Plugin | hooks.json or hooks/hooks.json (depends on plugin format) |
Workspace hooks take precedence over user hooks for the same event type. Agent-scoped hooks run in addition to workspace/user hooks. Enable agent-scoped hooks with chat.useCustomAgentHooks: true.
{
"hooks": {
"Stop": [
{
"type": "command",
"command": "bash .github/hooks/scripts/my-hook.sh",
"windows": "powershell -NoProfile -File .github/hooks/scripts/my-hook.ps1",
"env": { "MY_VAR": "value" },
"timeout": 10
}
]
}
}| Property | Type | Description |
|---|---|---|
type |
string | Must be "command" |
command |
string | Default command (cross-platform fallback) |
windows |
string | Windows-specific command override |
linux |
string | Linux-specific override |
osx |
string | macOS-specific override |
cwd |
string | Working directory (relative to repo root) |
env |
object | Additional environment variables |
timeout |
number | Timeout in seconds (default: 30) |
OS-specific commands are selected based on the extension host platform, which may differ from the local OS in remote scenarios (SSH, Containers, WSL).
hooks:
Stop:
- type: command
command: "bash .github/hooks/scripts/my-stop.sh"
windows: "powershell -NoProfile -File .github/hooks/scripts/my-stop.ps1"
timeout: 10-
agent_typenotagent_name: SubagentStop/SubagentStart input usesagent_typefor the agent name. There is noagent_namefield. -
Stop vs SubagentStop output format: Stop wraps in
hookSpecificOutput; SubagentStop uses top-leveldecision/reason. Using the wrong format silently fails. -
Agent-scoped Stop = SubagentStop: A Stop hook defined in an agent's frontmatter fires as SubagentStop when the agent runs as a subagent. Use the SubagentStop output format in these scripts.
-
stop_hook_activeinfinite loop guard: Always checkstop_hook_activein Stop/SubagentStop hooks. Iftrue, return{}immediately. Blocking causes the agent to continue, consuming premium requests indefinitely. -
Matchers are ignored in VS Code: Claude Code matcher syntax (
"Edit|Write") is parsed but not applied. Hooks run on every matching event regardless of the matcher value. -
Tool names differ across clients: VS Code uses
create_file,replace_string_in_file; Claude Code usesWrite,Edit. Checktool_namein PreToolUse/PostToolUse input, not assumed names. -
Tool input property casing differs: Claude Code uses snake_case (
tool_input.file_path); VS Code tools use camelCase (tool_input.filePath). -
Empty JSON
{}is the safe no-op output: Return{}(or empty stdout with exit 0) to allow the operation to proceed without modification. -
Most restrictive wins: When multiple control mechanisms are combined (
continue, exit codes,hookSpecificOutput), the most restrictive decision takes effect. -
Hook scripts are editable by the agent: If the agent can edit files in
.github/hooks/scripts/, it can modify its own hook scripts mid-session. Usechat.tools.edits.autoApproveto require manual approval for hook script edits. -
updatedInputsilently ignored on schema mismatch: If a PreToolUse hook returnsupdatedInputthat doesn't match the tool's expected input schema, it is silently ignored. Open the agent logs and find the logged tool schema to determine the correct format. -
Copilot CLI hook format auto-converted: VS Code converts Copilot CLI hook configs automatically —
preToolUse(lowerCamelCase) becomesPreToolUse(PascalCase). Thebashproperty maps toosxandlinux;powershellmaps towindows. -
Hooks work across agent types: Hooks fire for local agents, background agents, and cloud agents.
-
chat.hookFilesLocationscontrols discovery: Customizable setting to enable/disable specific hook file paths. Set a path tofalseto disable, including default locations.chat.useCustomizationsInParentRepositoriesenables hook discovery from parent repos in monorepos.
- View loaded hooks: Open Output panel → select "GitHub Copilot Chat Hooks" channel. Look for "Load Hooks" entries.
- View hook output: Same output channel shows hook execution results and errors.
- JSON parse errors: Ensure stdout is valid JSON. Use
jqor language JSON libraries to construct output. Stderr is not parsed as JSON.