Design, implement, debug, and manage VS Code Copilot hooks — automated behaviors that fire at key lifecycle points during agent sessions. Use this skill whenever someone wants to add auto-formatting, auto-linting, safety gates, test-on-change, session-start context injection, or end-of-session health checks. Trigger on: "add a hook", "auto-format", "auto-lint", "run tests automatically", "block dangerous commands", "hook not firing", or any request about automated agent behaviors.
Design and implement hooks — automated behaviors that fire at key lifecycle points during agent sessions. Hooks enforce quality without requiring the user to remember to ask.
Without hooks, quality depends entirely on the model remembering instructions. Hooks make quality automatic: every edit gets formatted, every dangerous command gets blocked, every session ends with a health check. They're the difference between "please format your code" and code that's always formatted.
VS Code Copilot provides 8 hook points — significantly more than Claude Code's 3:
| Event | When it fires | Best for |
|---|---|---|
| SessionStart | New session begins | Context injection, resource initialization, state validation |
| UserPromptSubmit | User submits a message | Prompt auditing, context injection based on prompt content |
| PreToolUse |
| Before any tool executes |
| Permission gates, dangerous operation blocking, input validation |
| PostToolUse | After a tool completes | Auto-format, auto-lint, test running, result validation |
| PreCompact | Before context compaction | Memory preservation, state export |
| SubagentStart | Subagent is spawned | Validate subagent has appropriate tools, initialize resources |
| SubagentStop | Subagent completes | Validate output quality, aggregate results, cleanup |
| Stop | Session ends | Health checks, memory capture, drift detection, reports |
.github/hooks/*.json~/.copilot/hooks/hooks field in .agent.md frontmatter{
"hooks": {
"PostToolUse": [
{
"type": "command",
"command": "./hooks/auto-format.sh",
"linux": "./hooks/auto-format.sh",
"osx": "./hooks/auto-format-mac.sh",
"windows": "powershell -File hooks\\auto-format.ps1",
"timeout": 10,
"env": { "CUSTOM_VAR": "value" }
}
]
}
}
Input (JSON via stdin):
{
"timestamp": "2026-03-31T10:30:00.000Z",
"cwd": "/path/to/workspace",
"sessionId": "session-id",
"hookEventName": "PostToolUse",
"tool_name": "editFiles",
"tool_input": { "files": ["src/main.ts"] },
"tool_response": "File edited successfully"
}
Output (JSON via stdout):
{
"continue": true,
"systemMessage": "Optional warning for the model",
"hookSpecificOutput": {
"additionalContext": "Context injected into the session"
}
}
Survey the project for automation opportunities:
| Question | If yes → Hook type |
|---|---|
| Does the project have a formatter? | PostToolUse auto-format |
| Does the project have a linter? | PostToolUse auto-lint |
| Are there destructive CLI commands to guard? | PreToolUse command gate |
| Are secrets a concern? | PreToolUse secret scanner |
| Is there a test suite? | PostToolUse test-on-change |
| Does the project need memory? | SessionStart + Stop hooks |
| Are subagents used? | SubagentStart/Stop validation |
| Is context budget a concern? | PreCompact memory preservation |
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
# Only run after file edits
if [[ "$TOOL_NAME" != "editFiles" && "$TOOL_NAME" != "createFile" ]]; then
echo '{}'
exit 0
fi
# Extract edited file paths
FILES=$(echo "$INPUT" | jq -r '.tool_input.files[]? // .tool_input.path // ""')
for FILE in $FILES; do
[ -z "$FILE" ] && continue
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx|*.json|*.css|*.md)
npx prettier --write "$FILE" 2>/dev/null || true ;;
*.py)
ruff format "$FILE" 2>/dev/null || true ;;
*.rs)
rustfmt "$FILE" 2>/dev/null || true ;;
*.go)
gofmt -w "$FILE" 2>/dev/null || true ;;
esac
done
echo '{}'
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
if [[ "$TOOL_NAME" != "editFiles" ]]; then
echo '{}'
exit 0
fi
FILES=$(echo "$INPUT" | jq -r '.tool_input.files[]? // ""')
ISSUES=""
for FILE in $FILES; do
[ -z "$FILE" ] && continue
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx)
RESULT=$(npx eslint "$FILE" 2>/dev/null || true)
[ -n "$RESULT" ] && ISSUES="$ISSUES\n$RESULT" ;;
*.py)
RESULT=$(ruff check "$FILE" 2>/dev/null || true)
[ -n "$RESULT" ] && ISSUES="$ISSUES\n$RESULT" ;;
esac
done
if [ -n "$ISSUES" ]; then
jq -n --arg issues "$ISSUES" '{
"hookSpecificOutput": {
"additionalContext": ("Lint issues found:\n" + $issues + "\nPlease fix these issues.")
}
}'
else
echo '{}'
fi
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
# Only check file writes
if [[ "$TOOL_NAME" != "editFiles" && "$TOOL_NAME" != "createFile" ]]; then
echo '{}'
exit 0
fi
CONTENT=$(echo "$INPUT" | jq -r '.tool_input | tostring')
# Check for common secret patterns
if echo "$CONTENT" | grep -qE 'AKIA[0-9A-Z]{16}|sk-[a-zA-Z0-9]{48}|-----BEGIN.*PRIVATE KEY-----|password\s*=\s*["\x27][^"\x27]{8,}'; then
jq -n '{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": "Potential secret or credential detected in file content. Remove secrets before writing."
}
}'
else
echo '{}'
fi
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
# Only check command execution
if [[ "$TOOL_NAME" != "runCommand" ]]; then
echo '{}'
exit 0
fi
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# Block destructive commands
if echo "$COMMAND" | grep -qE 'rm -rf /|rm -rf \.|git push.*--force|git reset --hard|DROP TABLE|DELETE FROM.*WHERE|kubectl delete|terraform destroy'; then
jq -n --arg cmd "$COMMAND" '{
"hookSpecificOutput": {
"permissionDecision": "deny",
"permissionDecisionReason": ("Dangerous command blocked: " + $cmd)
}
}'
# Prompt for risky commands
elif echo "$COMMAND" | grep -qE 'git push|npm publish|docker push|terraform apply'; then
jq -n --arg cmd "$COMMAND" '{
"hookSpecificOutput": {
"permissionDecision": "ask",
"permissionDecisionReason": ("This command affects shared state: " + $cmd)
}
}'
else
echo '{}'
fi
| Need | Event | Why |
|---|---|---|
| Format/lint after edits | PostToolUse | React to completed edits |
| Block dangerous operations | PreToolUse | Prevent before execution |
| Inject project context | SessionStart | Available from the start |
| Capture memories | Stop | Last chance before session ends |
| Validate subagent permissions | SubagentStart | Before subagent runs |
| Quality-check subagent output | SubagentStop | Before results used |
| Preserve state before compaction | PreCompact | Save before context is trimmed |
| Inject context per-prompt | UserPromptSubmit | Tailored to what user asked |
set -euo pipefailINPUT=$(cat)jq# Test with sample input
echo '{"tool_name": "editFiles", "tool_input": {"files": ["src/main.ts"]}}' | ./hooks/auto-format.sh
# Test blocking
echo '{"tool_name": "runCommand", "tool_input": {"command": "rm -rf /"}}' | ./hooks/command-gate.sh
hooks/chmod +x hooks/your-hook.sh.github/hooks/When a hook isn't working:
ls -la hooks/your-hook.shcat .github/hooks/your-hook.json | jq .which jqPostToolUse), not camelCasedeny for security, ask for risky ops, and additionalContext for style feedback.echo '{}' && exit 0.cwd field from stdin, not hardcoded paths.linux, osx, windows fields for cross-platform hooks.echo | ./hooks/script.sh.