Skip to main content

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โ€‹

RuleTier 0Tier 1Tier 2Tier 3Tier 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:

ModeBehaviorUse it when
softEvery 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.
onBlock on the first failing check โ€” fail-fast, the legacy behavior.You want the local hook to stop you before you push, mirroring CI exactly.
offThe 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):

  1. $BEWITH_HOOKS_MODE โ€” a one-off override for a single command, e.g. BEWITH_HOOKS_MODE=on git commit โ€ฆ.
  2. ~/.bewith/hooks-mode โ€” the per-developer choice written by bewith hooks.
  3. git-hooks/default-mode โ€” the team-wide default, shipped inside the plugin. Because the plugin is always-latest, changing this one file in bewith-docs and 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 from soft to on once the soft landing has bedded in, or to disable a misbehaving check fleet-wide without a code change to the check itself.
  4. 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 by bewith 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 โ€” wraps git checkout -b and git switch -c, validates the proposed branch name against the same regex branch-name-check.sh uses, 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:

  1. .claude/hooks/capability-awareness.sh โ€” SessionStart hook that teaches Claude to surface existing skills/agents/policies when the user describes intent.
  2. (planned) a parallel .claude/hooks/active-gates-awareness.sh that 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โ€‹