Skip to content

Agent Hooks

VS Code Agent Hooks automate code quality enforcement by running shell commands at key lifecycle points during agent sessions. They complement the instruction-based approach with deterministic, code-driven automation.

Agent hooks are a VS Code Preview feature. Agent-scoped hooks (defined in .agent.md frontmatter) require chat.useCustomAgentHooks: true in .vscode/settings.json.

sequenceDiagram
    participant A as 🤖 Agent
    participant H as 🪝 Hook
    participant T as 🔧 Tool

    A->>H: PreToolUse (before tool runs)
    alt Blocked
        H-->>A: deny + reason
    else Allowed
        H-->>A: continue
        A->>T: Execute tool
        T-->>A: Result
        A->>H: PostToolUse (after tool runs)
        H-->>A: continue + advisory
    end
Hook DirectoryEventPurposeTimeout
block-dangerous-commands/PreToolUseBlock dangerous terminal commands and hook self-modification10s
post-edit-format/PostToolUseAuto-format .md, .bicep, .tf, .js files after edits30s
session-start-audit/SessionStartLog session, inject project context (step, subscription, branch)5s
subagent-validation/SubagentStopValidate subagent output quality (advisory)15s
session-report/StopGenerate lightweight session summary180s

Hooks are registered in .vscode/settings.json:

{
"chat.hookFilesLocations": {
".github/hooks/block-dangerous-commands": true,
".github/hooks/post-edit-format": true,
".github/hooks/session-start-audit": true,
".github/hooks/subagent-validation": true,
".github/hooks/session-report": true
},
"chat.useCustomAgentHooks": true
}

Agents can define hooks in their YAML frontmatter (requires chat.useCustomAgentHooks):

hooks:
PostToolUse:
- type: command
command: ".github/hooks/post-edit-format/post-edit-format.sh"
timeout: 30

If a hook is already registered globally in chat.hookFilesLocations, do not re-define it in agent frontmatter — this causes the hook to run twice. Use agent-scoped hooks only for agent-specific logic not covered by global hooks.

The Stop hook (session-report.sh) checks stop_hook_active from stdin JSON. If true, it returns immediately without processing — preventing infinite re-invocation.

The PreToolUse hook blocks file-edit tools (replace_string_in_file, create_file, etc.) from modifying files under .github/hooks/. Path resolution uses realpath to handle symlinks and traversal attacks (../).

Each hook specifies a timeout (5-180s). If a hook exceeds its timeout, VS Code terminates it and continues the agent session.

The Stop hook sanitizes sessionId input to prevent path traversal — only alphanumeric characters, hyphens, and underscores are preserved.

Each hook follows this pattern:

.github/hooks/{name}/
├── hooks.json # Event binding + timeout
└── {name}.sh # Shell script (must be executable)
{
"hooks": {
"<EventName>": [
{
"type": "command",
"command": ".github/hooks/{name}/{name}.sh",
"timeout": 30
}
]
}
}

Valid event names: PreToolUse, PostToolUse, SessionStart, SubagentStart, SubagentStop, Stop.

All hook scripts must:

  1. Start with #!/usr/bin/env bash
  2. Include set -euo pipefail
  3. Read JSON from stdin
  4. Write JSON to stdout ({"continue": true} or {"hookSpecificOutput": {...}})
  5. Be executable (chmod +x)
Terminal window
# Validate hook configurations
npm run validate:hooks
# Run hook integration tests
npm run test:hooks
Terminal window
mkdir -p .github/hooks/my-hook-name

Create .github/hooks/my-hook-name/my-hook-name.sh:

#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
# Parse input JSON
FIELD=$(echo "$INPUT" | python3 -c \
"import sys,json; print(json.load(sys.stdin).get('field',''))" \
2>/dev/null || echo "")
# Your logic here...
# Output JSON safely (prevents injection)
python3 -c "
import json, sys
print(json.dumps({'continue': True, 'systemMessage': sys.argv[1]}))
" "Your message" 2>/dev/null || echo '{"continue": true}'

Then: chmod +x .github/hooks/my-hook-name/my-hook-name.sh

{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": ".github/hooks/my-hook-name/my-hook-name.sh",
"timeout": 30
}
]
}
}

Add to chat.hookFilesLocations in .vscode/settings.json:

".github/hooks/my-hook-name": true

Add test cases to scripts/test-hooks.sh, then run:

Terminal window
npm run validate:hooks
npm run test:hooks
  • JSON safety: Always use python3 json.dumps() for output — never string interpolation
  • Fast execution: Keep hooks under their timeout; check tool availability with command -v
  • No network calls: Hooks should be fast and local
  • Path safety: Use realpath and verify paths are within the repository
  • Error handling: Use set -euo pipefail; handle missing tools gracefully
  1. Verify the hook directory is listed in chat.hookFilesLocations in .vscode/settings.json
  2. Check the script is executable: ls -la .github/hooks/{name}/{name}.sh
  3. View hook output: Output panel → GitHub Copilot Chat Hooks channel

If a hook exceeds its timeout, VS Code kills the process and continues. Check for:

  • Network calls in hooks (avoid — hooks should be fast and local)
  • Large file processing (the >1MB guard in post-edit-format prevents this)
  • Missing tool binaries (hooks should check command -v before running tools)

Test a hook locally by piping mock JSON:

Terminal window
echo '{"tool_name":"run_in_terminal","tool_input":{"command":"ls"}}' | \
bash .github/hooks/block-dangerous-commands/block-dangerous-commands.sh

Agent hooks (.github/hooks/) and git hooks (lefthook.yml) serve different purposes:

Agent HooksGit Hooks (lefthook)
WhenDuring agent sessionsOn git commit/push
ScopeIndividual tool invocationsStaged/changed files
Config.github/hooks/*/hooks.jsonlefthook.yml
PurposeReal-time quality enforcementPre-commit/pre-push validation

Both systems complement each other — agent hooks catch issues during authoring, git hooks catch issues before commit.