Labs / Lab 05

Add a hook or lifecycle automation layer

Put policy and logging around a tool call so safety and observability live outside the tool itself.

What you'll build

A hook runner that gates and logs tool calls.

By the end of this lab you will have a tiny wrapper that intercepts a tool call before and after execution. The before_tool_call() hook can block sensitive terms like password, secret, and token before the tool runs at all.

The after_tool_call() hook records every decision and every result to a JSONL log. That gives you a concrete example of how modern AI tools add approval gates, policy checks, and audit trails without rewriting the underlying capability.

Run it

cd ai_ecosystem_labs
python3 05-hook/hook_runner.py
Starting here? Quick setup
git clone https://github.com/BanditF/ai_ecosystem_labs
cd ai_ecosystem_labs
python3 05-hook/hook_runner.py

Requires Python 3.8+. No additional packages needed for this lab.

Time guide. Setup: ~2 min. Working through it: 20–35 min because the policy and logging layer adds extra moving pieces.

Why this piece exists

Good systems separate the tool from the policy around the tool.

If every tool has to enforce its own approval logic, content rules, and logging, those concerns get copied everywhere. Hooks give you stable moments around execution where the host can say yes, say no, or record what happened without changing the tool's actual job.

A familiar analog is Git hooks. pre-commit can block a bad commit before it lands, and post-commit can trigger follow-up automation after the fact. This lab uses the same pattern around a fake search tool: gate first, run if allowed, then log what happened.

The code

hook_runner.py

Walk through it

Four things worth noticing.

before_tool_call() is a gate

This hook runs before execution and can stop the tool entirely. Here it lowercases arguments.get("term", "") and blocks anything in BLOCKED_TERMS. That is the same basic pattern content filters, rate limiters, and approval gates all use: inspect the request, return a decision, and only continue if allow is true.

after_tool_call() is an observer

This hook does not change the outcome. It just takes the finished record and appends it to disk with log.write(json.dumps(record) + "\n"). That separation of concerns matters. Logging stays useful, but it does not get to break the happy path.

JSONL makes a simple audit trail

The log file is append-only and each line is one complete JSON object. That means tool_calls.jsonl is easy to grep, tail, or replay later. You do not need a database to get something audit-shaped. One object per line is often enough.

The hook is transparent to the tool

fake_tool() just counts terms in files. It has no idea a hook exists. The wrapper in run_tool() adds policy before the call and logging after the call. That is the key idea: the hook wraps around the tool instead of forcing the tool to know about the hook.

Expected output

What allowed and blocked calls look like in the audit log.

Your file paths may appear as absolute paths depending on where you run the script — that's expected.

An allowed call writes a JSONL line like this:

{"time": "2026-05-06T00:19:35Z", "tool": "term_count", "arguments": {"term": "agent", "files": ["labs/sample_docs/agents.txt"]}, "decision": {"allow": true, "reason": "read-only search"}, "result": {"ok": true, "summary": {"total": 2}}}

A blocked call still gets logged, but the decision changes:

{"time": "2026-05-06T00:19:35Z", "tool": "term_count", "arguments": {"term": "password", "files": ["labs/sample_docs/agents.txt"]}, "decision": {"allow": false, "reason": "blocked sensitive term: password"}, "result": {"ok": false, "error": "blocked_by_hook"}}

That is the point of the pattern. Even rejected calls leave a trace. You can see what was attempted, why it was blocked, and what the system did instead of running the tool.

Try this

Three things to try before moving on.

  1. Trigger a blocked call. Run the script with password as the term and then read the log entry. Notice that the decision contains "allow": false and the result comes back as blocked_by_hook.
  2. Add one more blocked term. Edit BLOCKED_TERMS, add a new sensitive word, and run the script again with that term. You should see the new term intercepted by before_tool_call() without changing the tool itself.
  3. Read the audit trail directly. Run several calls, then open tool_calls.jsonl. It is just a line-by-line history of decisions and results, which means you can grep, tail, or replay it later.

Concepts behind this

For the broader frame on hooks, wrappers, and where they sit in the stack, read Extensions.

For why agent runtimes depend on hooks like this for safety and observability, read Agents.