Beyond Vibe Coding Patterns for Building Real Software with Claude Code

The Control Stack

On this page

A book-level mental model: the four mechanisms by which a developer shapes Claude’s behavior, arranged on a single axis from advisory to binding. Living draft — captured here so it can seed its own chapter (or a recurring framework page referenced from many chapters). Status: v0, expected to be redrawn as a designed diagram before it ships.


The one-sentence version

You have four mechanisms for steering Claude — CLAUDE.md, skills, the CLI, and hooks — and they line up on a single axis: how hard is it for Claude to ignore this? Each layer is a backstop for the one above it. Used together, they are defense in depth for behavior.

The axis

ADVISORY ───────────────────────────────────────────────────► BINDING
(Claude can ignore it)                            (Claude cannot ignore it)

  CLAUDE.md          Skills            CLI                  Hooks
  instructs          guide             constrains           enforce
                                       + standardizes

  read, then         fire only         refuse bad input,    harness-run on
  maybe forgotten    when triggered    gate destructive     events; not
  or overridden      Claude can skip   ops — but only       bypassable; can
                                       when Claude calls     block a call or
                                       the command          inject context

The progression is the whole point: it runs from things Claude reads and may ignore to things the system does to Claude whether it likes it or not.

The four mechanisms

1. CLAUDE.md — instructs. Context injected where Claude already looks. Soft: Claude reads it and usually complies, but it can forget mid-session, lose it under context pressure, or override it when another instruction seems to outrank it. Cheapest to write, easiest to change, least reliable. Failure mode: silent non-compliance — the instruction was there, Claude just didn’t follow it this turn.

2. Skills — guide. A workflow encoded as procedure rather than prose. More structured than a CLAUDE.md line because it packages a sequence, but still advisory: it only helps when it actually triggers, and Claude can proceed without it. Failure mode: the skill never fires (wrong trigger), so the workflow it encodes simply doesn’t happen.

3. The CLI — constrains, and standardizes. Two jobs, often conflated:

  • Standardizes — gives Claude a deterministic, named surface to act through instead of improvising raw bash. The interface itself is the contribution.
  • Constrains — defines the legal action space and refuses what’s illegal or ambiguous; gates destructive operations behind explicit flags.

Real teeth — Endless’s task start stub that refuses and redirects to task claim is enforcement, not suggestion. But the teeth only bite when Claude chooses to call the command. Failure mode: Claude routes around the CLI entirely (ad-hoc bash, the wrong tool) and never hits the constraint.

4. Hooks — enforce. The only layer the harness runs rather than Claude. They fire on events (a tool call, a stop, a commit) regardless of Claude’s intent, and can block the action or inject context before it proceeds. This is the sole truly non-bypassable mechanism. Failure mode: rigidity and maintenance cost — a hook that’s too broad blocks legitimate work and becomes something the developer fights.

The load-bearing insight: each layer backstops the one above

The same instruction can live at every level, and the levels catch what the previous one lets slip. Endless’s “claim the task before you work” rule:

  • CLAUDE.md says “run endless task claim <id> before writing code.”
  • A skill walks through claiming as step one of the work-intake workflow.
  • The CLI refuses the stale task start verb and points at claim.
  • A hook could block any task mutation that wasn’t preceded by a claim.

Four shots at the same invariant, escalating from “Claude reads it” to “the system enforces it.” That’s defense in depth applied to behavior instead of security.

The design principle that falls out

Hard enforcement (hooks, hard refusals) is reliable but rigid and high-maintenance. Instruction (CLAUDE.md) is flexible but unreliable. So:

Reach for the lightest layer that is reliable enough for the stakes — and escalate to hard enforcement only for the invariants that genuinely matter.

Hook-enforcing everything turns your control stack into a cage you spend your days picking locks on. Most behavior wants instruction; reserve the binding layers for the few rules whose violation is expensive.

The caveat: enforcement that only fires for agents depends on detection

If a CLI refusal or a hook only triggers when the caller is an agent — say, when the environment variable CLAUDECODE=1 is set — then the whole rule depends on that variable still being there. A different agent harness, or a future change to how detection works, and the rule quietly stops firing. Nobody notices, because humans rarely exercise the path it was guarding.

The fix is to give a human a way to see the agent’s view on demand: a flag like --agent-view that renders the agent-only output for inspection. You can’t keep an enforcement path working if you can’t watch it run.

Where this lives in the book

This is a framework, not a single chapter’s argument. Introduce it early (Part I, or a standalone framework page in the front matter) so later chapters can reference “the control stack” by name. The deterministic-CLI chapter then zooms into one layer of it — the CLI as constraint-and-standardization surface — while pointing up and down the stack to show how CLAUDE.md, skills, and hooks cover what the CLI can’t.

Open questions

  • Is “Control Stack” the right name, or does “reliability stack” / “behavior-control surface” read better to a non-developer?
  • Do skills sit above or below the CLI on the bindingness axis? (They’re advisory like CLAUDE.md, but they often invoke the CLI — arguably they’re orchestration over the lower layers rather than a peer on the same line.)
  • Worth a fifth element for the model’s own training/defaults as the bottom layer (the behavior you get with no mechanism at all)?

Found something wrong, unclear, or plainly disagreeable? Open an issue