Skip to content

Course · Codex · Extending Codex

When a hook beats judgment or a permission

Recall the two hooks from the wiring lesson (or take them as given): a PreToolUse hook that inspects every shell command for a path into the raw bank statements and blocks the rewrite, and a PostToolUse hook that re-runs the money tests after every edit-tool edit under app/importers/ and app/services/. In the example run the first blocked two routes to the same forbidden rewrite, and let a third pass untouched - a script whose command line named no statement path, invisible to a check that reads the command string. The second caught a float walking back into the money parse inside the turn that wrote it. Neither is a loose end - they are the facts this lesson exists to explain. You have now governed Codex three different ways, and knowing which one a constraint actually needs is what turns “I set up a guardrail” into a guardrail that’s still there at 2am.

Lay the three side by side:

  • A rule (a line in AGENTS.md) is context. It tells the model what’s true and what you want, and the model weighs it when it decides. It lives in the context window - the real 21-line file costs about 115 estimated tokens every session, the rules chapter’s arithmetic - which means it can be compacted out, buried under a long session, or simply under-weighted on a given turn. You’re trusting the model to remember and to care.
  • The approvals and sandbox dials (the course’s trust controls, covered in their chapter) are environmental enforcement. They’re stronger than a rule - the sandbox is a genuine wall, and the agent can’t write outside its writable roots or hit the network when you’ve turned it off. But they govern capability: which tools the agent may invoke, where it may write, whether it may reach the network. They answer “may the agent do this kind of thing?” - not “is the content of this specific command acceptable?”
  • A hook is deterministic, model-independent code at a supported lifecycle point. Its handler runs outside the model’s reasoning and costs zero standing prefix - nothing about a hook rides the window just by existing. But a hook is not silent by nature: when one fires, its output can reach the model, as deny feedback, added context, or a continuation prompt, capped at roughly 2,500 tokens by default. The model’s reasoning, its mood, its remaining budget - none of that is inside the hook.

The sharpest way to feel the difference: a rule routes through the model’s judgment, the sandbox through a capability check. A hook routes through neither. That’s why the statements guarantee had to be a hook. “Never rewrite a raw statement” isn’t a fact to remember, and it isn’t a capability to revoke - reading statements is the legitimate work the widened budget-build writable roots were opened for. It’s a condition on the content of one command, checked at the instant it is attempted. Only a hook expresses that.

A number worth sitting with. The course’s 21-line AGENTS.md costs about 115 estimated tokens, every session, forever. The two hooks cost zero standing prefix - the scripts never enter the window by existing. When one fires, its denial or feedback adds tokens to that turn, and only that turn. A rule buys judgment, and you pay for the window whether or not the model uses it well. A hook buys a guarantee for free, and pays only when it speaks.

Stress-test the ladder below - here “permission” is Codex’s approvals-and-sandbox dial, and the clock is the part to play with, because what holds while you watch isn’t what holds in an unattended run:

Four constraints you want to hold, three homes each: a rule (the model is told), a permission (the harness forbids a class of action), a hook (your code runs on the rail, every time). All four start in the rules file. Re-home them - then move the clock, because what holds while you watch isn’t what holds at 2am.

situation
  • the fragile modulea preference · low stakes

    The auth module is fragile - prefer minimal, surgical diffs there.

    held - you’re the gate

    “Minimal and surgical” is a judgment call, not a checkable condition. You want the model informed and weighing it - and your diff review is the backstop for the times it doesn’t.

  • the secrets walla never · high stakes

    The agent must never read secrets/.

    held - you’re the gate

    An instruction the model weighs - so "never" actually means "unless a debugging trail leads there and, this turn, outweighs the line." Walls that matter don’t get to be suggestions.

  • the tested-commit gatea condition on content · high stakes

    No commit that touches money code unless the money tests pass.

    held - you’re the gate

    It works all morning, which is what makes it dangerous. By hour three it is one line among hundreds and loses to an eager streak - the untested commit lands at 2am with nobody to catch it.

  • the every-time formatteran every-time · low stakes

    Every file the agent writes gets formatted - every time, not most times.

    held - you’re the gate

    The model formats when it remembers, and “every time” done by memory is “most times.” No single miss hurts; the drift and the diff noise pile up.

holding4 of 4shaky0silently broken0

All four look fine - and that’s the trap state. While you watch, an instruction is indistinguishable from a guarantee, because you’re the enforcement. The file didn’t hold the line; you did. Move the clock.

Each tool wears its own names - permissions may be an approvals-and-sandbox dial, a hook a plugin on lifecycle events - but the ladder is the same: guarantee strength is set by what sits in the loop - the model’s memory, a harness wall, or your code.

Everything defaults to the rules file, which is where most constraints start life in a real project too. Move the clock past “watching” and re-home each one. Notice which constraints hold no matter where you put them, and which only look protected because you’re the one currently enforcing them by reading every diff.

So which do you reach for? The test is about what kind of guarantee the situation demands:

  • State a preference or a fact the agent should weigh - “money is integer cents, never float,” “the keyword table is fragile, be careful.” That’s a rule. You want the model informed, and you accept that it’s the model deciding.
  • Constrain what the agent can do or where it can reach - read-only on this run, no network, nothing outside the workspace and ~/budget-data. That’s the approvals and sandbox model. You want a wall around a capability or a path, with no per-command judgment involved.
  • Check a condition at a supported lifecycle point - no rewrite of a raw statement, the money tests after every money edit, a risky command flagged before it runs. That’s a hook. You want a deterministic guardrail that does not depend on the agent remembering anything; pair it with sandbox, file permissions, and CI when the condition is security-critical.

The fourth container from the last chapter slots in under these three: a skill packages a procedure. It doesn’t inform, bound, or enforce - it acts, on recognition. Four mechanisms, four verbs: rules inform, skills package, hooks enforce, sandbox bounds. The rule still needs a clear approval setting (the trust dials) around it; the hook needs no remembering but is no replacement for infrastructure-level enforcement. Hooks also run the positive version - firing a test suite after a matching action, not just blocking - the money-tests gate is exactly that shape.

Why this is the hinge of the whole chapter

Section titled “Why this is the hinge of the whole chapter”

That last property - a deterministic check at a supported hook point - is why hooks matter the moment you stop watching. Sitting at the TUI you are still part of the approval boundary; a forgotten rule you’d catch in the diff. Its value shows up when you’re not there - the report regenerating on a schedule, the import check running in CI, a turn finishing at 2am. In those runs, combine trusted hooks with sandboxing, least-privilege credentials, and CI gates - never one hook as the only wall.

Put the statements hook on the ladder above and it’s the one that holds identically in all three situations - watching, three hours and a compaction later, unattended at 2am - because the check runs on the rail, not in the window, and no compaction can reach it. The AGENTS.md money rule sitting next to it doesn’t get that guarantee, and never will, no matter how clearly it’s worded.

Which is the whole reason this chapter came before the next one. budgetcli (the course’s running-example repo) now holds both halves of what unattended work needs: reach - a rate-lookup tool wired over MCP (the Model Context Protocol, the external-tool connection standard from the reach lesson; a constructed feature, real wiring) - and gates - the statements wall and the money-tests check, which no argument can sway.

Reach and gates, priced against each other

Section titled “Reach and gates, priced against each other”

Both moves from this chapter changed what Codex can do, and they price almost nothing alike. The rate server costs roughly 128 estimated tokens for one loaded tool’s schema - standing from registration where schemas inject up front, from first use where Codex defers them - and every later turn keeps paying once the schema has loaded, whether the tool is called again or not. The hooks cost zero standing prefix (the scripts never ride the window); denial feedback adds tokens only to the turn it fires in. The server is read-only reach, its real risk being what you connect next and stop paying attention to; the gates shrink blast radius to nothing on the patterns they match, and anything the command strings do not literally contain still gets through.

That comparison is the chapter’s whole argument. Reach costs you window space, forever, whether you use it. A gate costs nothing while it stands silent, and pays only when it speaks - but only on the exact thing its strings can see. Neither one is free in the way that matters: one spends window, the other spends your foresight in writing the matcher correctly the first time.

The course’s villain from one last new angle: each session still starts cold except what is written down where Codex reads by default - and what is written down informs, never enforces. What’s in front of Codex now includes a rate door that wasn’t there and two walls it can’t argue with, both put there on purpose, both priced.

One thing in this whole stack has never happened yet: none of it has run without a human at the keyboard. What runs when nobody watches - and what fail-shut even means when there’s no one to fail in front of - is the automation chapter.