An introdcution to “Hooks” in IBM Bob.
Introduction
As AI-assisted software development rapidly evolves, AI coding assistants are no longer isolated prompt-and-response interfaces. They are becoming deeply integrated members of our development workflows. IBM Bob provides a robust extension mechanism through Lifecycle Hooks.
Hooks allow developers and team leads to inject real-time context, enforce strict safety policies, auto-format or lint modified files, and stream live session telemetry into external security and logging dashboards.
Unlike probabilistic Large Language Model responses, hooks act as deterministic rules executed by the system. Because LLMs can occasionally hallucinate or bypass soft instructions, hooks provide hard boundaries and guaranteed execution logic for critical tasks. Whether enforcing zero-trust compliance, blocking dangerous commands, running exact test suites, or formatting code according to team standards, hooks guarantee that specified automation and checks execute every single time without relying on model discretion.
In this blog post, we walk through to discover what lifecycle hooks are in IBM Bob, explore the architectural model behind them, and look at actual code implementations that demonstrate how hooks can bring governance and intelligence to your AI session.
What Are Lifecycle Hooks in IBM Bob?
In IBM Bob, Hooks are executable scripts or HTTP endpoints that run automatically in response to specific lifecycle events during an agent session.
Instead of letting an LLM agent operate as a black box, hooks give developers full control over the inputs and outputs at critical checkpoints:
-
Context Injection: Auto-inject workspace details, Node/Git metadata, and architectural rules directly into the LLM context.
-
Policy Guardrails: Intercept user prompts or tool executions to reject prohibited commands (e.g., destructive SQL queries or broad file deletion) before they hit the agent or workspace.
-
Automated Feedback Loops: Automatically lint modified files (such as running ESLint after
write_file) and supply issues back to Bob to self-correct. -
Auditability & Observability: Persist logs and stream event webhooks to central telemetry services.
Overall System Architecture
The mermaid chart below gives the big picture of the overall system architecture.
Lifecycle Execution State Diagram
The life cycle of an IBM Bob task moves systematically through pre-defined state checkpoints:
Hooks Decision Tree
The flow below, gives an overall view of hooks decision tree.
Practical Hook Implementations
This project shows how to use every Bob lifecycle hook in a practical, production-ready way. It is structured to be progressive — start with the simple SessionStart example and work toward advanced patterns like blocking prompts, gating tool calls, and chaining shell + HTTP hooks together.
| Hook | Script | Purpose |
| ------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `SessionStart` | [`.bob/hooks/session-start.mjs`](.bob/hooks/session-start.mjs) | Inject git/env context into the model (startup, resume, compact) |
| `UserPromptSubmit` | [`.bob/hooks/prompt-guard.mjs`](.bob/hooks/prompt-guard.mjs) | Block dangerous prompts (structured deny) or enrich with context |
| `PreCompact` | [`.bob/hooks/pre-compact.mjs`](.bob/hooks/pre-compact.mjs) | Gate compaction; block manual if uncommitted changes exist |
| `PostCompact` | [`.bob/hooks/post-compact.mjs`](.bob/hooks/post-compact.mjs) | Save compact_summary to `output/`; notify HTTP listener |
| `PreToolUse` | [`.bob/hooks/tool-gate.mjs`](.bob/hooks/tool-gate.mjs) | Block dangerous commands (structured deny) or rewrite tool input |
| `PostToolUse` | [`.bob/hooks/post-tool-observer.mjs`](.bob/hooks/post-tool-observer.mjs) | Run ESLint after writes; feed findings back to model |
| `Stop` | [`.bob/hooks/session-stop.mjs`](.bob/hooks/session-stop.mjs) | Generate session summary report; optional git auto-commit |
| HTTP listener | [`server.mjs`](server.mjs) | Receive forwarded events; serve live dashboard + report viewer |
| Automation runner | [`tests/run-all-hooks.mjs`](tests/run-all-hooks.mjs) | Run all 26 hook scenarios; write timestamped Markdown report |
| Shell entry-point | [`scripts/run-hooks.sh`](scripts/run-hooks.sh) | CLI wrapper: `--open`, `--browser`, `--session-id` flags |
| Report router | [`routes/reports.mjs`](routes/reports.mjs) | Secure Markdown→HTML renderer; `/report/latest`, `/report/list` |
The structure of the files provided;
bob-hook-implementation/
├── .bob/
│ ├── hooks/ # ← All hook scripts live here
│ │ ├── session-start.mjs # SessionStart hook (startup / resume / compact)
│ │ ├── prompt-guard.mjs # UserPromptSubmit hook (structured deny + context)
│ │ ├── pre-compact.mjs # PreCompact hook (compaction gate)
│ │ ├── post-compact.mjs # PostCompact hook (save compact_summary)
│ │ ├── tool-gate.mjs # PreToolUse hook (structured deny + input rewrite)
│ │ ├── post-tool-observer.mjs # PostToolUse hook (lint + telemetry)
│ │ └── session-stop.mjs # Stop hook (session report)
│ ├── settings.json # ← Bob hook configuration (all 7 events)
│ └── rules/
│ └── AGENTS.md # Project rules
├── Docs/
│ ├── Architecture.md # Mermaid architecture diagrams
│ ├── Quickstart.md # Step-by-step setup guide
│ └── HooksReference.md # Complete hook API reference
├── routes/
│ └── reports.mjs # ← Report router (marked + sanitize-html)
├── scripts/
│ ├── start.sh # Start server in detached mode
│ ├── stop.sh # Graceful shutdown
│ ├── cleanup.sh # Remove node_modules, output, logs
│ └── run-hooks.sh # ← Automation runner shell entry-point
├── tests/
│ ├── run-tests.mjs # 36 unit tests (one-off hook assertions)
│ └── run-all-hooks.mjs # ← 26-scenario automation runner + report writer
├── logs/ # Runtime logs (git-ignored)
├── output/ # Session JSON + Markdown reports (git-ignored)
├── input/ # Input documents (content git-ignored)
├── server.mjs # ← HTTP listener + dashboard + report viewer
├── package.json
├── .env.example # Environment variable template
└── README.md
Below are key code excerpts illustrating how hooks process incoming payloads from standard input (stdin) and communicate decisions back via standard output (stdout) or process exit codes.
-
Context Injection on
SessionStart: when a new session opens,session-start.mjscollects local environment metadata (Git branch, commit hash, Node version) and writes it to standard output. Bob captures this standard output and injects it into the prompt context.
// session-start.mjs
import { execSync } from "node:child_process";
import { existsSync, mkdirSync, appendFileSync } from "node:fs";
import { join } from "node:path";
// Read payload from standard input
let raw = "";
for await (const chunk of process.stdin) raw += chunk;
let payload = JSON.parse(raw || "{}");
const sessionId = payload.session_id ?? "unknown";
const cwd = payload.cwd ?? process.cwd();
const source = payload.source ?? "startup";
// Collect runtime git and toolchain context
function safeExec(cmd) {
try { return execSync(cmd, { encoding: "utf8", cwd }).trim(); }
catch { return "unavailable"; }
}
const gitBranch = safeExec("git rev-parse --abbrev-ref HEAD");
const gitLastCommit = safeExec("git log -1 --pretty=%s");
const nodeVersion = safeExec("node --version");
// Inject context directly into Bob model context via stdout
const context = `
=== Bob Session Context (auto-injected) ===
Session : ${sessionId}
Source : ${source}
Node.js : ${nodeVersion}
Git Branch : ${gitBranch}
Last Commit : ${gitLastCommit}
===========================================
`.trim();
process.stdout.write(context + "n");
process.exit(0);
-
Prompt Safety Guardrail with
UserPromptSubmit: before a prompt reaches the model,prompt-guard.mjsinspects the prompt string for prohibited commands or security breaches. It can either return a structured JSON block or enrich the query with company guidelines.
// prompt-guard.mjs
let raw = "";
for await (const chunk of process.stdin) raw += chunk;
let payload = JSON.parse(raw || "{}");
const prompt = payload.prompt ?? "";
// Safety check against destructive instructions
const BLOCKED_PATTERNS = [
{ pattern: /bdrops+tableb/i, reason: "Destructive SQL not permitted." },
{ pattern: /rms+-rfs+/(?!S)/, reason: "Root directory deletion blocked." }
];
const match = BLOCKED_PATTERNS.find(({ pattern }) => pattern.test(prompt));
if (match) {
// Structured JSON block response (exit code 0 with decision: "block")
const blockResponse = JSON.stringify({
decision: "block",
reason: `Prompt blocked by policy: ${match.reason}`,
});
process.stdout.write(blockResponse + "n");
process.exit(0);
}
// Structured enrichment response (attaches additional contextual instructions)
const enrichResponse = JSON.stringify({
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: "[Policy]: Ensure code includes unit tests and follows style guidelines.",
},
});
process.stdout.write(enrichResponse + "n");
process.exit(0);
-
Automated Code Quality Feedback with
PostToolUse: when Bob executes a file mutation tool likewrite_file,post-tool-observer.mjsexecutes code verification tools (such as ESLint) against the file. Any discovered linting issues are piped tostdoutand automatically added to the conversation so Bob can fix them immediately.
// post-tool-observer.mjs
import { spawnSync } from "node:child_process";
import { extname, basename, join } from "node:path";
let raw = "";
for await (const chunk of process.stdin) raw += chunk;
let payload = JSON.parse(raw || "{}");
const toolName = payload.tool_name ?? payload.tool ?? "";
const toolInput = payload.tool_input ?? payload.input ?? {};
if (["write_file", "apply_diff"].includes(toolName)) {
const filePath = String(toolInput.path ?? "");
const ext = extname(filePath);
if ([".js", ".mjs", ".ts"].includes(ext)) {
// Run automated linter on the generated/edited file
const eslint = spawnSync("npx", ["--no", "eslint", "--format", "compact", filePath], {
encoding: "utf8",
timeout: 15000,
});
if (eslint.status !== 0 && eslint.stdout) {
// Pipe linter findings back to stdout for model feedback
process.stdout.write(
`[PostToolUse/ESLint] Linting issues found in ${basename(filePath)}:n${eslint.stdout}n`
);
}
}
}
process.exit(0);
- Telemetry and Webhook Ingestion Listener: all hooks can send async HTTP signals to a central telemetry endpoint. Below is a sample Node.js/Express server configured to consume lifecycle events and track statistics:
// server.mjs (Express Listener Endpoint)
import express from "express";
import { appendFileSync } from "node:fs";
const app = express();
app.use(express.json());
app.post("/hooks", (req, res) => {
const event = {
id: `evt_${Date.now()}`,
received_at: new Date().toISOString(),
...req.body,
};
console.log(`▶ Hook Event Received: ${event.hook_event_name || event.event} | Session: ${event.session_id}`);
// Persist for centralized compliance and SIEM auditing
appendFileSync("logs/http-events.log", JSON.stringify(event) + "n");
return res.status(200).json({ ok: true, event_id: event.id });
});
app.listen(3001, () => console.log("Bob Hook Telemetry Server listening on port 3001"));
You can either test the hooks individually or run the test as batch (adapted to your use-cases) and verify the outputs before implementing for real use-cases.
Conclusion
IBM Bob’s hook system provides a robust architecture for bringing governance, contextual intelligence, and automated verification into your AI development environment. By employing deterministic script execution at key lifecycle checkpoints, software engineering teams can confidently adopt AI tooling while enforcing organizational policies, code quality standards, and centralized security monitoring.
To explore configuration options, event payloads, and setup guides, check out the official IBM Bob Lifecycle Hooks Documentation.
Thanks for reading 🪝🐟
Links
- Github repository for this post: https://github.com/aairom/Bob-hook-implementation/tree/main
- IBM Bob: https://bob.ibm.com/
- IBM Bob Hooks: https://bob.ibm.com/docs/ide/configuration/lifecycle-hooks







