FAI Hooks Deep Dive
Eleven security and governance hooks across five shipped lifecycle events.
What Are Hooks?
Hooks are automated scripts that fire at specific points in a Copilot session lifecycle. They run shell commands or Node.js modules to enforce policies — scanning for secrets, blocking dangerous tool calls, redacting PII, or logging audit trails. Unlike agents (which respond to user prompts) or instructions (which shape code output), hooks operate silently in the background, intercepting events before they cause harm.
Each hook lives in its own folder under .github/hooks/ containing a hooks.json configuration and one or more executable scripts. The hooks.json file declares which lifecycle events trigger the hook and what command to run.
hooks.json Schema
Every hook folder must contain a hooks.json file with the following structure:
{ "version": 1, "hooks": { "Stop": [ { "type": "command", "command": "bash hooks/fai-secrets-scanner/scan-secrets.sh", "windows": "powershell -File hooks/fai-secrets-scanner/scan-secrets.ps1", "cwd": ".", "env": { "SCAN_MODE": "warn", "SCAN_SCOPE": "diff" }, "timeout": 30 } ] }}| Field | Type | Required | Description |
|---|---|---|---|
| version | number | Yes | Schema version (always 1) |
| hooks.<Event> | array | Yes | One or more commands keyed by lifecycle event |
| type | string | Yes | Command entry type |
| command | string | Yes | Default command to execute |
| windows / env / timeout | mixed | No | Platform override, environment, and execution bound |
The 5 Shipped Lifecycle Events
The 11 repository manifests use five lifecycle events. Each event has distinct timing and runtime context:
| Event | When It Fires | Input (stdin) | Use Cases |
|---|---|---|---|
| SessionStart | When a session begins | Runtime session context | Initialize audit, logging, and token-budget controls |
| UserPromptSubmit | After prompt submission | Prompt event context | Audit prompts and session activity |
| PreToolUse | Before tool execution | Tool name and input | Block dangerous operations before side effects |
| PostToolUse | After tool execution | Tool input and output | Record a redacted tool trace |
| Stop | When execution stops | Runtime stop context | Validate output, scan, audit, and record cost |
All 11 FAI Hooks
The repository contains 11 hook manifests covering security, governance, cost, quality, and tool observability:
| # | Hook | Event | WAF Pillar | Description |
|---|---|---|---|---|
| 1 | fai-secrets-scanner | Stop | Security | Scan the completed interaction for secret patterns |
| 2 | fai-tool-guardian | PreToolUse | Security | Block dangerous tool operations before execution |
| 3 | fai-tool-observer | PostToolUse | Operational | Write bounded, redacted tool traces |
| 4 | fai-governance-audit | Multiple | Operational | Audit session start, prompts, and stop events |
| 5 | fai-license-checker | Stop | Security | Check dependency licenses at stop |
| 6 | fai-waf-compliance | Stop | Reliability | Check WAF evidence at stop |
| 7 | fai-session-logger | Multiple | Operational | Log start, prompt, and stop events |
| 8 | fai-cost-tracker | Stop | Cost | Record usage cost when execution stops |
| 9 | fai-pii-redactor | Stop | Responsible AI | Apply configured PII redaction at stop |
| 10 | fai-token-budget-enforcer | SessionStart | Cost | Initialize and enforce the session token budget |
| 11 | fai-output-validator | Stop | Responsible AI | Validate output against configured quality controls |
Hook Script Structure
Hook scripts follow a simple contract: read input from stdin, process it, and exit with a status code. Exit 0 means pass (allow), exit 1 means block (reject). Any output to stdout is logged; output to stderr is shown to the user on block.
#!/usr/bin/env nodeconst fs = require("fs");
// Read user prompt from stdinlet input = "";process.stdin.on("data", (chunk) => (input += chunk));process.stdin.on("end", () => { const patterns = [ /(?:sk|pk|api)[_-]?[a-zA-Z0-9]{20,}/g, // API keys /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g, // GitHub tokens /DefaultEndpointsProtocol=https;Account/g, // Azure conn strings /-----BEGIN (?:RSA )?PRIVATE KEY-----/g, // Private keys /eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+/g, // JWT tokens ];
const mode = process.env.HOOK_MODE || "warn"; const found = patterns.some((p) => p.test(input));
if (found) { console.error("[secrets-scanner] Potential secret detected in prompt"); process.exit(mode === "block" ? 1 : 0); }
console.log("[secrets-scanner] Clean — no secrets found"); process.exit(0);});Hook Execution Flow
When a lifecycle event fires, the runtime discovers all hooks registered for that event and executes them in a deterministic order:
- Event fires — Copilot runtime emits e.g.
userPromptSubmitted - Hook discovery — Runtime scans all
.github/hooks/*/hooks.jsonfor matching events - Order resolution — Hooks execute alphabetically by folder name (use numeric prefixes to control order)
- Input piping — Event context (prompt text, tool args, session log) is piped to stdin
- Script execution — The
commandruns with declaredenvvariables - Exit code check — Exit 0 = pass, exit 1 = block (session continues or halts)
- Chain continues — If pass, next hook runs. If block, remaining hooks are skipped
Warn vs Block Mode
Every FAI hook supports two execution modes controlled by the HOOK_MODE environment variable:
| Mode | Exit Code | Behavior | Use When |
|---|---|---|---|
| warn | Always 0 | Log the violation but allow the action to proceed | Development, testing, initial rollout |
| block | 1 on violation | Log the violation and halt the action | Production, compliance-required environments |
{ "version": 1, "hooks": [ { "event": "preToolUse", "command": "node guard-tools.js", "description": "Block dangerous terminal commands", "env": { "HOOK_MODE": "block", "BLOCKED_PATTERNS": "rm -rf,DROP TABLE,--force,--no-verify" } } ]}Hook Chaining Order
Multiple hooks can listen to the same event. They execute in alphabetical order by folder name. To control execution order, use numeric prefixes:
hooks/ fai-01-pii-redactor/ # Runs first — redact PII fai-02-secrets-scanner/ # Runs second — scan for secrets fai-03-tool-guardian/ # Runs third — check tool safety fai-04-cost-guardian/ # Runs fourth — enforce budget fai-05-output-validator/ # Runs last — validate responseIf hook #2 returns exit 1 (block), hooks #3-5 are skipped. The chain short-circuits on the first blocking failure. This fail-fast behavior ensures that critical security hooks can prevent all subsequent processing.
WAF Pillar Mapping
Each hook enforces specific WAF pillars, making the governance model traceable from architecture decisions down to runtime enforcement:
| WAF Pillar | Hooks | Enforcement |
|---|---|---|
| Security | secrets-scanner, tool-guardian, license-checker | Block secrets, dangerous commands, risky packages |
| Reliability | waf-compliance | Verify all required WAF pillars are configured |
| Cost Optimization | cost-guardian, token-budget | Track spend, enforce per-session token limits |
| Operational Excellence | governance-audit, session-logger | Audit trails, structured logging, correlation IDs |
| Responsible AI | pii-redactor, output-validator | PII protection, groundedness and safety gates |
Example: Tool Guardian Hook
The tool guardian intercepts preToolUse events and blocks dangerous commands before they execute:
#!/usr/bin/env nodelet input = "";process.stdin.on("data", (chunk) => (input += chunk));process.stdin.on("end", () => { const { tool, args } = JSON.parse(input); const mode = process.env.HOOK_MODE || "block"; const blocked = (process.env.BLOCKED_PATTERNS || "").split(",");
// Only inspect terminal commands if (tool !== "run_in_terminal") { process.exit(0); }
const command = args.command || ""; const match = blocked.find((p) => command.includes(p.trim()));
if (match) { console.error( "[tool-guardian] BLOCKED: " + JSON.stringify(match) + " found in command: " + command.substring(0, 80) ); process.exit(mode === "block" ? 1 : 0); }
console.log("[tool-guardian] Allowed: " + tool); process.exit(0);});Wiring Hooks into Solution Plays
Hooks are listed in the primitives.hooks array of fai-manifest.json. Different plays can activate different hook combinations — a public-facing chatbot play might enable all hooks, while an internal analytics play might only use cost-guardian and session-logger.
{ "play": "01-enterprise-rag", "primitives": { "hooks": [ "fai-secrets-scanner", "fai-tool-guardian", "fai-pii-redactor", "fai-cost-guardian", "fai-output-validator" ] }}Creating Custom Hooks
Build your own hook in 3 steps:
# 1. Create the hook foldermkdir -p .github/hooks/my-custom-hook
# 2. Create hooks.jsoncat > .github/hooks/my-custom-hook/hooks.json << 'EOF'{ "version": 1, "hooks": [ { "event": "userPromptSubmitted", "command": "node check.js", "description": "My custom validation" } ]}EOF
# 3. Create the script (read stdin, exit 0 or 1)cat > .github/hooks/my-custom-hook/check.js << 'EOF'let input = "";process.stdin.on("data", (c) => (input += c));process.stdin.on("end", () => { // Your validation logic here const isValid = !input.includes("forbidden-word"); process.exit(isValid ? 0 : 1);});EOF
# 4. Validatenpm run validate:primitives