Enforcement Tiers
The platform enforces conventions at multiple layers. The same rule can fire at more than one tier โ each layer catches what the previous one missed, and earlier tiers are strictly cheaper because they save the developer (or Claude) from the cost of unwinding state that was created on a wrong assumption.
The tier ladder, from earliest to latest:
Tier 0 shell interceptor opt-in, ~/.zshrc / ~/.bashrc
catches at command time, before state exists
Tier 1 editor / IDE LSP, eslint-on-save, prettier
catches while typing
Tier 2 pre-commit hook git-hooks/*.sh
catches before commit
Tier 3 pre-push hook git-hooks/pre-push
catches before push
Tier 4 PR CI .github/workflows/*.yml
catches at PR open / push to PR branch
Tier 5 branch protection GitHub settings
catches at merge (the final backstop)
The architectural principle, already documented in ai-engineering-strategy.md ยง16: every check that can be expressed as a local hook also has a CI mirror. Hooks fire locally for developers who installed them; CI fires unconditionally for anyone โ Copilot, Claude mobile, browser-edits โ who didn't.
This document goes one step further: every check should also be evaluated for Tier 0 / Tier 1 fit, because there are command-time interceptions that no commit-time hook can do.
Current rules โ where each one firesโ
| Rule | Tier 0 | Tier 1 | Tier 2 | Tier 3 | Tier 4 |
|---|---|---|---|---|---|
Branch naming ({type}/{slug}[_CU-{id}]) | opt-in (helper) | โ | โ branch-name-check.sh | โ same hook | โ validate-pr.yml |
| No edits on master | โ | โ | โ no-master-edit.sh (commit) | โ | โ branch protection (Tier 5) |
| File size cap | โ | โ | โ file-size-check.sh | โ | โ mirrored |
| Capability metadata + index | โ | โ | โ capability-index-check.sh | โ | โ mirrored |
| Policy schema | โ | โ | โ policy-schema-check.sh (added 2026-05-29) | โ | โ validate-pr.yml |
| CU-id at end of PR title | โ | โ | โ | โ | โ clickup-commit-check.yml |
| Lint pass (project-local) | โ | โ eslint-on-save (if dev configured) | โ lint-pass.sh | โ | โ mirrored |
| Tests pass (project-local) | โ | โ | โ tests-pass-check.sh | โ | โ mirrored |
| Typecheck pass (project-local) | โ | โ tsc LSP | โ typecheck-pass.sh | โ | โ mirrored |
| Block dangerous git/bash | โ | โ | โ | โ | โ |
| (above is enforced via Claude Code PreToolUse hooks, parallel ladder) |
Empty cells are not necessarily gaps to fill โ sometimes a tier doesn't apply (e.g. you can't intercept "no edits on master" at command time, because every editor goes through different I/O paths). They're places to consider whether a check fits.
The Tier 2/3 git hooks run in soft mode by default โ they warn, they do not block. See Local hook modes below. Tier 4 (CI) and Tier 5 (branch protection) are the hard floor regardless.
Local hook modes (off ยท soft ยท on)โ
The Tier 2/3 git hooks are local โ they help the developer catch issues before CI does. Aggressive local blocking turns that help into antagonism: a hook that refuses your commit over a lint nit you were about to fix anyway is a tax, not a safety net. So the local hooks default to a soft landing, and the hard enforcement lives where it can't be skipped โ Tier 4 (CI) and Tier 5 (branch protection), which fire for everyone (Copilot, browser edits, a teammate who never installed the hooks) regardless of any local setting.
Three modes:
| Mode | Behavior | Use it when |
|---|---|---|
soft | Every check runs; failures print a warning but never block the commit/push. (default) | The normal day-to-day. You see problems early; CI is still the gate. |
on | Block on the first failing check โ fail-fast, the legacy behavior. | You want the local hook to stop you before you push, mirroring CI exactly. |
off | The orchestrator no-ops entirely โ no checks run. | A hook is in your way and you need it gone right now. |
Changing the modeโ
The developer-facing control is the bewith hooks CLI:
bewith hooks # show the current mode + wiring state
bewith hooks soft # warn-only (the default)
bewith hooks on # enforce locally (block like CI)
bewith hooks off # disable โ hooks stay wired but no-op
bewith hooks uninstall # remove the wiring entirely (unset core.hooksPath)
bewith hooks reinstall # re-wire + reset to soft
off vs uninstall: off leaves the hooks wired but inert โ the orchestrators no-op no matter how they were reached (global core.hooksPath or per-repo symlink), so it's the master kill switch, and it's instantly reversible with bewith hooks soft. uninstall additionally unsets the machine-global core.hooksPath, for a developer who runs their own git hooks and doesn't want bewith's wiring at all. Both survive the SessionStart re-wire (git-hooks-sync): when the mode is off, wire-global-git-hooks.sh skips re-wiring and leaves core.hooksPath untouched.
How the mode is resolvedโ
git-hooks/hooks-lib.sh resolves the active mode in this order (first hit wins):
$BEWITH_HOOKS_MODEโ a one-off override for a single command, e.g.BEWITH_HOOKS_MODE=on git commit โฆ.~/.bewith/hooks-modeโ the per-developer choice written bybewith hooks.git-hooks/default-modeโ the team-wide default, shipped inside the plugin. Because the plugin is always-latest, changing this one file inbewith-docsand merging propagates the new default to every developer on their next plugin update โ for anyone who hasn't set a personal override (1 or 2). This is the lever to tighten the whole team fromsofttoononce the soft landing has bedded in, or to disable a misbehaving check fleet-wide without a code change to the check itself.softโ hardcoded fallback if nothing above resolves.
Per-repo check configuration (which checks are enabled, thresholds) is separate and lives in git-hooks/config.env (REQUIRE_CU_ID, REQUIRE_TYPECHECK, FILE_SIZE_LIMIT_KB, โฆ). Mode governs block vs warn vs skip; config governs which checks and how strict.
Scope: these modes govern the git hooks (Tier 2/3). The Claude Code PreToolUse hooks (
block-edit-on-master,block-dangerous-bash, โฆ) are a separate control surface and are not affected bybewith hooks.
Tier 0 (opt-in shell helpers)โ
Tier 0 is optional and per-developer. Forcing shell rewrites on every machine is hostile; we ship the helpers as opt-in installables in templates/shell-helpers/ and let developers source them from their own ~/.zshrc / ~/.bashrc. Devs who don't install them still pay the cost in Tiers 2โ4, which is the same cost they'd pay without Tier 0 anyway.
Current Tier 0 helpers (see templates/shell-helpers/README.md for install):
git-branch-interceptor.shโ wrapsgit checkout -bandgit switch -c, validates the proposed branch name against the same regexbranch-name-check.shuses, prompts with valid options when invalid. Catches the bad name before any state (the branch) is created.
When to add a new tier for an existing ruleโ
When a check feels expensive โ when developers (or Claude sessions) hit it repeatedly and have to unwind state โ that's the signal that a lower tier would pay back. Concrete examples:
- A check fires only at PR CI (Tier 4) but the developer commits + pushes multiple times before the PR opens โ consider Tier 2 (pre-commit).
- A check fires at commit-time but the developer just created a branch named wrong and now has to rename and re-push โ consider Tier 0 (interceptor).
- A check exists in CI but Claude reliably misses it when authoring โ consider injecting awareness via SessionStart hook (orthogonal to enforcement โ it's "telling the model" rather than "blocking the action").
When NOT to shift leftโ
- High false-positive checks are worse at lower tiers (interrupt typing for non-issues). Keep them at PR CI.
- Domain-knowledge checks (e.g. "this count and that list should agree") can't be expressed mechanically โ code review territory, not gates.
- Slow checks (full test suite, integration tests) belong at Tier 4 only. A pre-commit that takes 30 seconds is worse than the CI delay.
Awareness โ separate from enforcementโ
Tier 0โ5 are gates: they block invalid actions โ except the Tier 2/3 local hooks, which default to warn (soft mode) so the hard block lives at Tier 4/5 where it can't be skipped. Awareness is informing the developer that gates exist, so the gate trip is never a surprise.
Two awareness mechanisms today:
.claude/hooks/capability-awareness.shโ SessionStart hook that teaches Claude to surface existing skills/agents/policies when the user describes intent.- (planned) a parallel
.claude/hooks/active-gates-awareness.shthat surfaces "these are the gates that will run on your next commit, given the files you're touching." Not in this document's first pass โ tracked as a follow-up proposal.
Orthogonal โ session safetyโ
The tier ladder above governs gates on user-visible actions (commit, push, PR, merge). A separate axis โ session safety โ governs how a Claude Code session manages its own state vs. concurrent sessions on the same repo. That discipline lives in session-safety.md: write-side isolation via temporary git worktrees, and read-side freshness verification against current state. Neither belongs in the tier ladder because both fire at the session boundary itself, before a tier-0 helper could ever see the command.
Pointersโ
- ai-engineering-strategy.md ยง16 โ original rationale for hook/CI parity
git-hooks/โ Tier 2/3 implementationstemplates/shell-helpers/โ Tier 0 opt-in helpers.github/workflows/validate-pr.ymlโ Tier 4 mirrorsession-safety.mdโ the parallel session-boundary discipline