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 DirectoryEvent(s)PurposeTimeout
secrets-scanner/StopScan for leaked secrets at session end30s
session-telemetry/SessionStart, Stop, UserPromptSubmitMerged session lifecycle logging and governance audit5–10s
subagent-validation/SubagentStopValidate subagent output quality (advisory)15s
tool-audit/PostToolUseLog tool usage metadata (name, status)5s
tool-guardian/PreToolUseBlock dangerous terminal commands10s

Hooks are registered in .vscode/settings.json:

{
"chat.hookFilesLocations": {
".github/hooks/secrets-scanner": true,
".github/hooks/session-telemetry": true,
".github/hooks/subagent-validation": true,
".github/hooks/tool-audit": true,
".github/hooks/tool-guardian": true
},
"chat.useCustomAgentHooks": true
}

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

hooks:
PreToolUse:
- type: command
command: "bash .github/hooks/tool-guardian/guard-tool.sh"
timeout: 10

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-telemetry/session-end.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.

Secrets are caught at two independent layers:

  • Layer 1 — Pre-commit (gitleaks): Blocks known secret patterns in staged files before they enter git history. Local soft-skip if gitleaks is not installed; CI hard-fail.
  • Layer 2 — Session end (secrets-scanner): Regex-based scanner at Stop event catches in-session writes that may not yet be staged. Logs to logs/copilot/secrets/scan.log.

Each hook follows this pattern:

.github/hooks/{name}/
├── hooks.json # Event binding + timeout
└── {name}.sh # Shell script (present at configured path; exec bit optional)
{
"hooks": {
"<EventName>": [
{
"type": "command",
"command": "bash .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 present at the configured path and be invoked through bash in hooks.json so execution does not depend on the file mode
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}'
{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "bash .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 tools/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 exists and the configured command uses bash: 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
  • 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.