Imported from domattioli/DomI (
skills/enforce-branch-policy/SKILL.md). Install upstream withnpx skills add domattioli/DomI --skill enforce-branch-policy. Copyright stays with the author.
enforce-branch-policy
Session-start guard. Reads CLAUDE.md branch policy → checks current branch → corrects violations automatically. Prevents the branch sprawl pattern where system-injected branches override documented policy.
Metadata
- Category: Git / Session Setup
- Use Case: Called at every session start, before any development work
- Dependencies:
git, readableCLAUDE.mdin repo root - Scope: Local repo only; does not create new branches, only switches to policy branch
Why This Exists
CHILmesh and ADMESH accumulated 5+ orphaned claude/* branches per session. Root cause: Claude Code system initialization specifies a branch (e.g., claude/clever-mendel-a7Wc6) and Claude follows it, even when CLAUDE.md explicitly mandates a different branch (e.g., planning-optimize_modernize or development). This pattern repeated despite CLAUDE.md calling it out explicitly.
DomI issue #13 documents 13 session comments confirming this is a persistent infrastructure failure. The fix must be automated: CLAUDE.md reading + branch switching must happen at session start, not after the work is done.
When to Use
- First step at every session start (before
bash scripts/instructions_on_start.sheven) - When current branch looks like
claude/<adjective>-<name>-<hash>(system-injected pattern) - After any branch confusion:
/enforce-branch-policyto reset to policy branch
When NOT to Use
- When explicitly creating a PR branch for deliberate PR work (temporary exception; pass
--allow-pr-branch) - On repos without a
CLAUDE.md(skill warns and exits cleanly — no CLAUDE.md = no policy = no enforcement)
Policy Extraction
L1 policy sources are co-equal (v1.2, spec-016). CLAUDE.md and docs/branching.md both count as L1 — docs/branching.md is the canonical branching doc (root branching.md, when present, is a pointer stub only, not a policy source). Read both when present. If they disagree, docs/branching.md wins (it is the maintained, downstream-synced source; CLAUDE.md branch prose can lag a migration — see #195/#196, where daily-maintenance was deprecated in favor of development and per-repo CLAUDE.md files were not all updated in lockstep). The DomI-standard resolution for autonomous sessions is development.
Read CLAUDE.md and extract branch policy using pattern matching:
Patterns recognized (in order of precedence):
1. "work ONLY on `<branch>`"
2. "ALL Claude Code sessions MUST work exclusively on `<branch>`"
3. "default branch is `<branch>`"
4. "develop on `<branch>`"
5. "Branching — default branch is `<branch>`"
6. "Always use branch `<branch>`"
If multiple patterns found → use first match (highest precedence). If no pattern found → warn "no branch policy in CLAUDE.md" and exit 0 (no enforcement possible).
Conflict Resolution
Detect conflict between system-injected branch and policy branch:
Current branch: claude/clever-mendel-a7Wc6 ← system injected
Policy branch: development ← from CLAUDE.md
→ CONFLICT DETECTED
Resolution:
[enforce-branch-policy] Branch conflict:
System branch: claude/clever-mendel-a7Wc6
Policy branch: development (from CLAUDE.md)
CLAUDE.md wins. Switching branches...
$ git checkout development
Switched to branch 'development'
[enforce-branch-policy] ✓ Now on policy branch: development
Deviation logged: system-branch overridden at session start
Flow
Step 1: Read policy
POLICY_BRANCH=$(grep -oP '(?<=`)[ a-zA-Z0-9/_-]+(?=`)' CLAUDE.md | grep -m1 -E 'development|main|planning-')
# Fallback: parse explicit "work on" / "default branch" patterns
If no policy found:
[enforce-branch-policy] No branch policy found in CLAUDE.md. No enforcement applied.
Exit 0.
Step 2: Get current branch
CURRENT=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
If HEAD is detached:
[enforce-branch-policy] HARD STOP: HEAD is detached. Checkout policy branch manually: git checkout development
Step 2.5: Verify topology before concluding absence
Before treating the policy branch as nonexistent — and before any fallback to a harness-injected claude/* branch — verify against the remote, not local refs:
git fetch --prune origin && git ls-remote --heads origin
Fresh cloud containers are partial-ref clones: git branch -r shows only main plus the injected claude/* branch even when origin/development genuinely exists. Local refs are NOT evidence of absence (docs/branching.md #195 reopen, 2026-06-13; the ADMESH PRs #151/#153 incident traced to this exact gap). Never inherit a topology claim ("branch doesn't exist") from a prior session's note — re-verify with git ls-remote every session.
Step 3: Compare
If CURRENT == POLICY_BRANCH:
[enforce-branch-policy] ✓ On policy branch: development. No action needed.
Exit 0.
Step 4: Switch
git checkout "$POLICY_BRANCH"
If switch fails (branch doesn't exist locally):
git checkout -b "$POLICY_BRANCH" "origin/$POLICY_BRANCH" 2>/dev/null \
|| git checkout -b "$POLICY_BRANCH"
If still fails:
[enforce-branch-policy] HARD STOP: cannot switch to policy branch '$POLICY_BRANCH'.
Tried: checkout, checkout -b from origin, checkout -b fresh.
All failed. Manual resolution required.
Step 5: Log deviation
Append to session-end report:
- pre-flight: branch-policy-conflict
system branch: claude/clever-mendel-a7Wc6
policy branch: development
resolution: switched automatically
System Branch Pattern
System-injected branches match: claude/[a-z]+-[a-z]+-[A-Z0-9]{5,}
Examples:
claude/clever-mendel-a7Wc6claude/compassionate-lamport-UV2gIclaude/epic-ritchie-UV2gI
These are always system-assigned; CLAUDE.md policy always overrides them.
Hard Stops
| Condition | Message |
|---|---|
| HEAD detached | HARD STOP: detached HEAD — checkout policy branch manually |
git checkout fails after all fallbacks |
HARD STOP: cannot switch to policy branch — manual resolution required |
| CLAUDE.md is unreadable | Warn: "cannot read CLAUDE.md" then exit 0 (no enforcement) |
Integration
Add to scripts/instructions_on_start.sh as step 0:
# Step 0: Branch policy enforcement (before any other checks)
echo "=== Branch Policy ==="
/enforce-branch-policy || exit 1
Also add to consumer repo CLAUDE.md:
At session start, immediately run `/enforce-branch-policy` before any other work.
Introspect skill maps pre-flight: branch-policy-conflict pain to this skill in the known-pain table.
Limitations
- Policy extraction is pattern-based; unusual CLAUDE.md formats may not be recognized
- Does not check if uncommitted changes on wrong branch should be stashed first (skill warns if dirty working tree, user decides)
- Cannot prevent future system-injection; must be called at every session start
Version History
- v1.2 (2026-07-03) — spec-016 WS-2.
docs/branching.mdadded as a co-equal L1 policy source alongside CLAUDE.md (canonical branching doc; rootbranching.mdis a pointer stub only);docs/branching.mdwins on disagreement, DomI-standard resolution isdevelopment. New Step 2.5 ("Verify topology before concluding absence"):git fetch --prune origin && git ls-remote --heads originbefore concluding a policy branch doesn't exist or falling back to a harnessclaude/*branch — partial-ref clones make local refs lie (#195 reopen 2026-06-13, ADMESH PRs #151/#153 incident).detect_conflict.shscript parity (wiring the new L1 co-source + topology check into the script) is deferred — this bump is doc-only. - v1.1 (2026-05-21) — 3-layer reconciliation. Adds
scripts/detect_conflict.shthat programmatically compares L1 (CLAUDE.md), L2 (routine prompt via$CLAUDE_USER_BRANCHor--user-branch), L3 (current HEAD viagit rev-parseor--current-branch). Exit codes encode the verdict:0=in agreement,1=auto-switched,2=L1↔L2 BLOCK,3=no policy. Addstests/smoke.sh(12 scenarios A–L). Closes #13 reopen 2026-05-20 (vote comment 4493943591 — "decision-time conflict detection across all 3 instruction layers"). - v1.0 — Initial. Policy extraction from CLAUDE.md, system-branch detection, auto-switch with fallback, deviation logging. Origin: DomI issue #13, 13 session comments 2026-05-02..2026-05-11.
CLI
detect_conflict.sh # auto-detect all 3 layers
detect_conflict.sh --user-branch <name> # override L2 (also: $CLAUDE_USER_BRANCH)
detect_conflict.sh --claude-md <path> # alt CLAUDE.md (fixture)
detect_conflict.sh --current-branch <n> # override L3 (testing)
detect_conflict.sh --no-act # report only — never `git checkout`
Exit codes:
| Exit | Meaning |
|---|---|
| 0 | L3 already matches resolved policy |
| 1 | L1/L2 agree; L3 differed; auto-switched (or would have, with --no-act) |
| 2 | L1 ↔ L2 disagree → HARD BLOCK; operator must reconcile |
| 3 | No L1 AND no L2 → no enforceable policy |
