gent.
Example 1: Security Gate with PreToolUse
The PreToolUse event fires before any tool invocation. This is the optimal point to implement security policies. Unlike PostToolUse, which reacts to changes, PreToolUse prevents actions entirely.
Configuration:
{
"hooks": {
"PreToolUse": [
{
"type": "command",
"command": "./scripts/security-gate.sh"
}
]
}
}
Script Implementation (security-gate.sh):
This script parses the tool input and blocks operations matching a dangerous pattern. It uses jq to ensure valid JSON output, which is critical for hook reliability.
#!/usr/bin/env bash
set -euo pipefail
# Read JSON payload from stdin
PAYLOAD=$(cat)
# Extract tool name and command using jq for safe parsing
TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name')
COMMAND=$(echo "$PAYLOAD" | jq -r '.tool_input.command // empty')
# Define dangerous patterns
DANGEROUS_PATTERNS='^(sudo\s|rm\s+-rf\s|DROP\s+DATABASE|git\s+push\s+--force)'
# Check if the tool is a terminal command and matches dangerous patterns
if [[ "$TOOL_NAME" == "run_in_terminal" && "$COMMAND" =~ $DANGEROUS_PATTERNS ]]; then
# Construct deny response using jq to guarantee valid JSON
echo "$PAYLOAD" | jq '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "Security policy violation: Destructive command blocked."
}
}'
exit 0
fi
# Allow all other operations
echo "$PAYLOAD" | jq '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "allow"
}
}'
Rationale:
jq for Output: Manually constructing JSON strings in shell scripts is error-prone. Using jq ensures the output is always valid JSON, preventing hook failures due to syntax errors.
- Regex Matching: Using bash regex allows for flexible pattern matching without spawning external processes, keeping latency low.
- Exit Code 0: The script exits with
0 even when denying. The denial is communicated via the permissionDecision field in the JSON output.
Example 2: Runtime Context Injection with SessionStart
Instructions are static. Hooks can inject dynamic context that only exists at runtime, such as the current git branch, active environment, or CI pipeline status.
Configuration:
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "./scripts/inject-runtime-context.sh"
}
]
}
}
Script Implementation (inject-runtime-context.sh):
This script gathers environment data and injects it into the session context.
#!/usr/bin/env bash
# Gather dynamic context
CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "detached")
NODE_VERSION=$(node -v 2>/dev/null || echo "unknown")
CI_STATUS=${CI:-"local"}
# Inject context using additionalContext field
jq -n \
--arg branch "$CURRENT_BRANCH" \
--arg node "$NODE_VERSION" \
--arg ci "$CI_STATUS" \
'{
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: ("Runtime Context: Branch=" + $branch + ", Node=" + $node + ", CI=" + $ci)
}
}'
Rationale:
additionalContext: This field appends information to the system prompt for the session, ensuring the agent is aware of environmental constraints without manual prompting.
- Graceful Degradation: The script handles cases where
git or node might not be available, preventing hook failures in minimal environments.
Example 3: Agent-Scoped Hooks
For specialized agents, hooks can be defined directly in the agent definition. This is useful for agents with specific responsibilities, such as a "Release Manager" agent that must always run integration tests.
Agent Definition (.github/agents/release-manager.agent.md):
---
name: "Release Manager"
description: "Manages release workflows and validates deployment readiness."
hooks:
PostToolUse:
- type: command
command: "./scripts/validate-release-artifacts.sh"
---
You are the Release Manager agent. You coordinate release builds and ensure artifacts meet quality gates.
Rationale:
- Scope Isolation: Hooks defined here only execute when this specific agent is active, preventing unnecessary overhead for other agents.
- YAML Syntax: Agent hooks use YAML syntax within the frontmatter, mapping directly to the JSON structure used in workspace hooks.
Pitfall Guide
Implementing hooks requires careful attention to execution semantics and security. The following pitfalls are common in production environments.
-
Exit Code Confusion
- Mistake: Using exit code
1 expecting to block a tool call.
- Explanation: Exit code
1 generates a non-blocking warning. The agent continues execution. Exit code 2 is required to block the tool call and surface a message to the model.
- Fix: Use exit code
2 for hard blocks. Write the reason to stderr when using exit code 2.
-
The Self-Modification Attack
- Mistake: Allowing the agent to edit hook scripts without restriction.
- Explanation: If the agent has write access to hook scripts, it can modify them during a session to bypass its own security policies.
- Fix: Configure
chat.tools.edits.autoApprove to require manual approval for edits to hook scripts. Store hooks in read-only directories where possible.
-
JSON Output Malformation
- Mistake: Returning invalid JSON from the hook script.
- Explanation: VS Code expects strictly formatted JSON. Malformed output causes the hook to fail silently or produce errors in the output channel.
- Fix: Always use
jq or a JSON library to construct output. Validate JSON structure during development.
-
Input Sanitization Neglect
- Mistake: Using
eval or unsanitized variables from tool_input.
- Explanation: Hook scripts receive input from the agent, which may contain malicious payloads. Direct evaluation can lead to command injection.
- Fix: Treat all input as untrusted. Use safe parsing methods like
jq. Avoid eval and quote all variables.
-
Multiple Hook Precedence Misunderstanding
- Mistake: Assuming the first hook's decision wins when multiple hooks are defined.
- Explanation: VS Code evaluates all hooks for an event. The most restrictive decision wins:
deny overrides ask, and ask overrides allow.
- Fix: Design hooks with this precedence in mind. Use
deny for critical security policies and ask for warnings.
-
Performance Degradation
- Mistake: Running heavy scripts on high-frequency events like
PostToolUse.
- Explanation: Hooks add latency to every tool invocation. Slow scripts can make the agent feel unresponsive.
- Fix: Optimize scripts for speed. Use caching where possible. Avoid network calls in hooks unless necessary.
-
Stderr vs. JSON Response Ambiguity
- Mistake: Writing to
stderr but exiting with code 0.
- Explanation: Messages to
stderr with exit code 0 are logged but do not affect agent behavior. To block or warn, you must use the correct exit code or JSON response.
- Fix: Use exit code
2 with stderr for simple blocks. Use JSON permissionDecision for complex logic.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Team-wide security policy | Workspace Hooks (PreToolUse) | Applies to all agents, centralized management. | Low maintenance overhead. |
| Specialized agent behavior | Agent Hooks (.agent.md) | Scoped to specific agent, reduces noise. | Higher per-agent configuration. |
| User confirmation required | permissionDecision: "ask" | Balances automation with human oversight. | Increases interaction latency. |
| Hard block required | permissionDecision: "deny" | Zero tolerance for policy violations. | May require user intervention to proceed. |
| Dynamic context injection | SessionStart hook | Provides runtime data unavailable in static instructions. | Minimal latency impact. |
Configuration Template
A comprehensive hooks.json template demonstrating multiple events and best practices.
{
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "./scripts/inject-context.sh"
}
],
"PreToolUse": [
{
"type": "command",
"command": "./scripts/security-gate.sh"
},
{
"type": "command",
"command": "./scripts/audit-commands.sh"
}
],
"PostToolUse": [
{
"type": "command",
"command": "./scripts/format-and-lint.sh"
}
],
"Stop": [
{
"type": "command",
"command": "./scripts/generate-report.sh"
}
]
}
}
Quick Start Guide
- Create Directory: Initialize
.github/hooks/ in your repository root.
- Add Configuration: Create
hooks.json with a simple PostToolUse hook pointing to a script.
- Write Script: Create the script (e.g.,
format.sh), make it executable (chmod +x), and implement the logic.
- Test: Open VS Code Chat and trigger an action that invokes the hook. Check the "GitHub Copilot Chat Hooks" output channel for execution logs.
- Iterate: Expand the configuration to include
PreToolUse for security and SessionStart for context injection based on your workflow needs.