Skip to content

Latest commit

 

History

History
263 lines (194 loc) · 10.1 KB

File metadata and controls

263 lines (194 loc) · 10.1 KB
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

VS Code Agent Hooks

Reference for writing and reviewing hook scripts and agent-scoped hook configurations. Source: https://code.visualstudio.com/docs/copilot/customization/hooks

Lifecycle Events

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

Common Input Fields (All Events)

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.

Event-Specific Input Fields

SessionStart

  • source (string): How the session started. Currently always "new".

UserPromptSubmit

  • prompt (string): The text the user submitted.
  • Output: uses common output format only (no hookSpecificOutput).

PreToolUse / PostToolUse

  • 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).

PreCompact

  • trigger (string): How compaction was triggered ("auto" when conversation exceeds prompt budget).

SubagentStart

  • 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").

SubagentStop

  • agent_id (string): Unique identifier for the subagent instance.
  • agent_type (string): The agent name.
  • stop_hook_active (boolean): true when the subagent is already continuing from a previous stop hook. Check this to prevent infinite loops.

Stop

  • stop_hook_active (boolean): true when the agent is already continuing from a previous stop hook. Check this to prevent infinite loops.
  • Stop does NOT provide agent_id or agent_type — unless it is an agent-scoped Stop hook (see below).

Agent-Scoped Stop Hooks Are Also SubagentStop

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.

Output Formats

Common Output (All Events)

{
  "continue": true,
  "stopReason": "Security policy violation",
  "systemMessage": "Warning displayed to user"
}
  • continue: false stops the entire agent session (drastic).
  • systemMessage displays a warning regardless of other decisions.

Exit Codes

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

Stop Output

Uses hookSpecificOutput wrapper:

{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "decision": "block",
    "reason": "Run the test suite before finishing"
  }
}

SubagentStop Output

Uses top-level decision / reason (no hookSpecificOutput wrapper):

{
  "decision": "block",
  "reason": "Verify subagent results before completing"
}

SessionStart / SubagentStart Output

Inject context via hookSpecificOutput:

{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "Project: my-app v2.1.0 | Branch: main"
  }
}

PreToolUse Output

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).

PostToolUse Output

{
  "decision": "block",
  "reason": "Post-processing validation failed",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Lint errors found in edited file"
  }
}

Hook Configuration

File Locations (searched in order)

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.

JSON Configuration Format

{
  "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
      }
    ]
  }
}

Command Properties

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).

Agent Frontmatter Format

hooks:
  Stop:
    - type: command
      command: "bash .github/hooks/scripts/my-stop.sh"
      windows: "powershell -NoProfile -File .github/hooks/scripts/my-stop.ps1"
      timeout: 10

Gotchas and Common Mistakes

  1. agent_type not agent_name: SubagentStop/SubagentStart input uses agent_type for the agent name. There is no agent_name field.

  2. Stop vs SubagentStop output format: Stop wraps in hookSpecificOutput; SubagentStop uses top-level decision/reason. Using the wrong format silently fails.

  3. 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.

  4. stop_hook_active infinite loop guard: Always check stop_hook_active in Stop/SubagentStop hooks. If true, return {} immediately. Blocking causes the agent to continue, consuming premium requests indefinitely.

  5. 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.

  6. Tool names differ across clients: VS Code uses create_file, replace_string_in_file; Claude Code uses Write, Edit. Check tool_name in PreToolUse/PostToolUse input, not assumed names.

  7. Tool input property casing differs: Claude Code uses snake_case (tool_input.file_path); VS Code tools use camelCase (tool_input.filePath).

  8. Empty JSON {} is the safe no-op output: Return {} (or empty stdout with exit 0) to allow the operation to proceed without modification.

  9. Most restrictive wins: When multiple control mechanisms are combined (continue, exit codes, hookSpecificOutput), the most restrictive decision takes effect.

  10. 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. Use chat.tools.edits.autoApprove to require manual approval for hook script edits.

  11. updatedInput silently ignored on schema mismatch: If a PreToolUse hook returns updatedInput that 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.

  12. Copilot CLI hook format auto-converted: VS Code converts Copilot CLI hook configs automatically — preToolUse (lowerCamelCase) becomes PreToolUse (PascalCase). The bash property maps to osx and linux; powershell maps to windows.

  13. Hooks work across agent types: Hooks fire for local agents, background agents, and cloud agents.

  14. chat.hookFilesLocations controls discovery: Customizable setting to enable/disable specific hook file paths. Set a path to false to disable, including default locations. chat.useCustomizationsInParentRepositories enables hook discovery from parent repos in monorepos.

Debugging

  • 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 jq or language JSON libraries to construct output. Stderr is not parsed as JSON.