FAI Hooks Deep Dive
10 security and governance hooks across 4 lifecycle events — the automated safety net for every Copilot session.
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": [
{
"event": "userPromptSubmitted",
"command": "node scan-secrets.js",
"description": "Scan user prompts for accidental secret inclusion",
"env": {
"HOOK_MODE": "block",
"PATTERNS_FILE": "secret-patterns.json"
}
},
{
"event": "sessionEnd",
"command": "node scan-session-log.js",
"description": "Scan full session log for secrets before persistence"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
| version | number | Yes | Schema version (always 1) |
| hooks[].event | string | Yes | Lifecycle event to listen for |
| hooks[].command | string | Yes | Shell command to execute |
| hooks[].description | string | No | Human-readable purpose |
| hooks[].env | object | No | Environment variables passed to script |
The 4 Lifecycle Events
Hooks attach to one of four events in the Copilot session lifecycle. Each event has a specific timing and receives different context data from the runtime:
| Event | When It Fires | Input (stdin) | Use Cases |
|---|---|---|---|
| sessionStart | When a Copilot chat session begins | Session metadata (workspace, user) | Load context, verify credentials, log audit start |
| userPromptSubmitted | After user sends a message, before LLM processes | The full user prompt text | PII detection, secret scanning, prompt validation |
| preToolUse | Before a tool call is executed | Tool name + arguments JSON | Block dangerous commands, enforce tool policies |
| sessionEnd | When the chat session closes | Full session log | Final secret scan, usage logging, cost tracking |
All 10 FAI Hooks
FrootAI ships 10 pre-built hooks covering security, governance, cost, and quality. Each hook maps to specific WAF pillars:
| # | Hook | Event | WAF Pillar | Description |
|---|---|---|---|---|
| 1 | secrets-scanner | userPromptSubmitted | Security | Detect API keys, tokens, connection strings in prompts |
| 2 | tool-guardian | preToolUse | Security | Block rm -rf, DROP TABLE, force-push, and dangerous commands |
| 3 | governance-audit | sessionEnd | Operational | Log all tool calls and decisions for compliance audit trail |
| 4 | license-checker | preToolUse | Security | Verify package licenses before npm/pip install |
| 5 | waf-compliance | sessionStart | Reliability | Verify WAF pillar coverage meets play requirements |
| 6 | session-logger | sessionStart/End | Operational | Structured logging with correlation IDs for observability |
| 7 | cost-guardian | preToolUse | Cost | Track token usage, enforce per-session cost budgets |
| 8 | pii-redactor | userPromptSubmitted | Responsible AI | Detect and redact personal information before LLM processing |
| 9 | token-budget | preToolUse | Cost | Enforce max_tokens limits per query tier |
| 10 | output-validator | sessionEnd | Responsible AI | Validate LLM outputs against groundedness and safety thresholds |
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 node
const fs = require("fs");
// Read user prompt from stdin
let 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 node
let 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 10 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 folder
mkdir -p .github/hooks/my-custom-hook
# 2. Create hooks.json
cat > .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. Validate
npm run validate:primitives