Write a Simple Hook
Hooks are the fastest way to add cross-cutting behaviour to Codebolt — logging, policy, redaction, alerts. You'll write a working hook in ~10 minutes.
You'll need: a Codebolt project open. Node.js 18+ if writing a code hook (YAML hooks need no setup).
What we're building
A hook that logs every file write an agent makes to a file under .codebolt/audit.log. Trivial, but end-to-end: create the hook, enable it, verify it fires.
Step 1 — pick a phase
File writes fire the after_file_write hook phase. See Hook Types for the full list.
Step 2 — write the hook
Create .codebolt/hooks/audit-writes.ts:
import fs from "node:fs/promises";
import path from "node:path";
export default {
phase: "after_file_write",
handler: async (ctx) => {
const logPath = path.join(ctx.workspace.root, ".codebolt", "audit.log");
const line = JSON.stringify({
timestamp: new Date().toISOString(),
agent: ctx.by_agent,
runId: ctx.runId,
path: ctx.path,
bytes: ctx.bytes,
}) + "\n";
await fs.appendFile(logPath, line);
return { verdict: "observe" };
},
};
What's here:
phase— when this hook fires.handler— the function that runs. Receives a context object specific to the phase.verdict: "observe"— we're only watching, not blocking or modifying.- Side effect — append to the audit log file.
Step 3 — enable the hook
Hooks in .codebolt/hooks/ are picked up when the project is loaded. Restart Codebolt or reload the project for new hooks to take effect.
Step 4 — trigger the hook
Run any agent that writes a file:
codebolt --prompt "add a comment to README.md saying 'hi from an agent'" --agent generalist
After it finishes, check the audit log:
cat .codebolt/audit.log
You should see one or more JSON lines, one per file write the agent made.
Step 5 — test it
Trigger a few different changes and confirm they all get logged:
- Modify a file in a subdirectory.
- Create a new file.
- Let the agent touch multiple files.
Each should produce an audit entry.
Variations
Block writes outside a specific directory
export default {
phase: "before_tool_call",
match: { tool: "codebolt_fs.write_file" },
handler: async (ctx) => {
const target = ctx.args.path;
if (!target.startsWith("src/")) {
return {
verdict: "deny",
reason: `Writes are only allowed under src/. Got: ${target}`,
};
}
return { verdict: "allow" };
},
};
Rate-limit a tool
const recentCalls: number[] = [];
export default {
phase: "before_tool_call",
match: { tool: "external_api.query" },
handler: async (ctx) => {
const now = Date.now();
while (recentCalls.length && now - recentCalls[0] > 60_000) {
recentCalls.shift();
}
if (recentCalls.length >= 10) {
return {
verdict: "deny",
reason: "Rate limit: external_api.query capped at 10/minute.",
};
}
recentCalls.push(now);
return { verdict: "allow" };
},
};
Note: this example uses in-memory state, which doesn't survive restarts. For persistent state, write to a file or use a database.
YAML-only hook (no code)
For simple pattern matching, YAML is enough:
# .codebolt/hooks/no-dot-env-reads.yaml
name: no-dot-env-reads
phase: before_tool_call
match:
tool: "codebolt_fs.read_file"
args:
path_ends_with: ".env"
verdict: deny
reason: "Reading .env files is blocked. If you need the value, read from process.env."
No code, no dependencies. Drop the file and it's active.
Testing hooks
For code hooks, test them as pure functions:
import hook from "./audit-writes";
test("logs a write", async () => {
const ctx = {
workspace: { root: "/tmp/test" },
by_agent: "generalist",
runId: "run_test",
path: "README.md",
bytes: 42,
};
const result = await hook.handler(ctx as any);
expect(result.verdict).toBe("observe");
// Verify the log file was written (if you want a full test)
});
See Hook Examples for more patterns.
Common pitfalls
- Writing to stdout. Hook code runs in a worker —
console.logis fine, but in stdio-transport contexts (local MCP servers), stdout is reserved. Usectx.log.*for safety. - Slow hooks. Hooks run on the critical path. Don't make synchronous network calls. Defer expensive work with fire-and-forget.
- Modifying state that isn't yours. Hooks should be idempotent and side-effect-free where possible. Auditing to a file is fine; mutating another tool's state is asking for trouble.
- Too-broad matches. A hook with no
matchfires on every event of its phase — usually fine for observation, often wrong for denial.