Skip to content

refactor(claude): triage always-loaded user rules into skills, hooks, and ambient - #378

Merged
laurigates merged 2 commits into
mainfrom
claude/github-issue-353-9o437n
Aug 20, 2026
Merged

refactor(claude): triage always-loaded user rules into skills, hooks, and ambient#378
laurigates merged 2 commits into
mainfrom
claude/github-issue-353-9o437n

Conversation

@laurigates

Copy link
Copy Markdown
Owner

Closes #353. Paired with laurigates/claude-plugins#2450, which carries the skills and hooks these stubs point at — merge that one first, or the stubs point at nothing.

Result

114,284c → 76,564c across the 38 always-loaded rules — ~9,430 est. tokens/session, 33% off the surface, in every project, on top of the ~8.2k already landed for the portfolio rules.

The issue projected ~10,400. The gap is accounted for: prefer-diy-over-heavy-dependency (7,079c in the issue's table) was already promoted before this work started, so its ~1,600 tokens were already banked. Several other rules had grown since the issue was measured on 2026-08-05 — read-issue-thread-before-contributing 3,916 → 7,431, taskwarrior-tracking 2,978 → 4,735 — so the per-rule figures below are measured now, not copied from the issue.

Bucket 2 — nine rules with a trigger recognisable in advance

Rule Before After Now lives in
read-issue-thread-before-contributing 7,431 618 git-plugin:git-issue-scoping
pr-merge-hazards 7,607 2,163 git-plugin:git-merge-hazards
multi-model-delegation 6,607 756 agent-patterns-plugin:multi-model-delegation (already existed)
verify-machine-facts-before-publishing 4,945 593 documentation-plugin:docs-verify-machine-facts
taskwarrior-tracking 4,735 620 taskwarrior-plugin:task-add / task-claim
documentation-authoring 3,093 505 documentation-plugin:docs-single-source
scaffold-fix-backport 2,960 590 code-quality-plugin:code-scaffold-backport
verify-upstream-before-patching 2,710 654 git-plugin:git-upstream-fix-check
repo-deletion-safety 2,576 649 git-plugin:git-repo-delete-check + a hook

multi-model-delegation was a free win the issue didn't have: it listed the rule as already-stubbed, but on disk it was still 6,607c of full body while an agent-patterns-plugin skill of the same name had covered 100% of it since 2026-07-17 — plus four sections the rule lacked. The dotfiles copy was the stale side. Verified section-by-section before deleting rather than taken on faith.

pr-merge-hazards keeps its two load-bearing gates inline (the gh pr list --head authority order for squash-merge detection, and UNSTABLEBLOCKED), following the worktree-stale-base-merge exception. Both are read at the instant of deciding whether to delete a branch or merge over red — there is no earlier moment at which to invoke a skill, and getting it wrong deletes unmerged work. Those copies are byte-identical to the skill's, so drift is a literal string diff.

Bucket 4 — tool-use-patterns split, not moved

9,917 → 7,713. Ambient traps stay resident; only lookup material left, with pointers in its place.

Stays Goes
Read-before-Edit; re-Read after a formatter WebFetch failure→fallback ladder
rg -r is --replace, not a bundled short flag agent fan-out burst-limit guidance
"a rejected flag looks exactly like no results" usage-limit mid-run recovery
parallel-batch cancellation; Bash denials are terminal

Smaller than the issue's ~1,300-token estimate (~555 gross, ~485 net of the new skill's listing cost) because the file had shrunk since it was measured. Still worth doing — no always-on trap was made lazy.

Bucket 1 — two hooks

Both in laurigates/claude-plugins#2450: repo-deletion-safety.sh (block, 57 assertions) and branch-base-guard.sh (nudge, 59 assertions). The repo-deletion-safety stub here names the hook, its self-extinguishing property, and its opt-out, so the rule reads as skill + enforcement rather than skill alone.

Bucket 3 — deliberately left resident

Rules whose trigger is the failure they prevent: diagnose-at-the-failure-point, never-fabricate-test-identifiers (by the time you'd think to load the skill you have already fabricated the identifier), plus communication, decision-defaults, security, plan-mode, agent-and-tool-selection. And skill-and-agent-catalog-check above all — promoting rules to skills only pays off if the catalog is actually consulted at task boundaries, which makes it load-bearing for this entire plan.

Every move is verifiable

Bodies were moved by script, never retyped, then diffed against a pre-change snapshot of all 49 rules. The only edits inside a moved body are sibling-rule path requalifications (git-hazards.md~/.claude/rules/git-hazards.md), since the text no longer sits beside those rules. That keeps "the skill cannot have drifted from the rule it replaced" a checkable claim rather than a hope.

A false premise found in the budget test

tests/test-claude-context-budget.sh carried a 2026-08-09 comment asserting that a cleanup had landed — "multi-model-delegation went 6,607 → 493 bytes … freeing 6.1 kB". It never did; the rule was still 6,607 bytes when this work started, so a TOTAL_BUDGET_BYTES bump to 120,000 was argued on bytes that were never freed.

Corrected and kept as a lesson rather than quietly deleted (a budget bump must cite measured bytes on disk, never a cleanup believed to have landed). Remeasured from disk and ratcheted 120,000 → 86,000 — 6.4% headroom, deliberately not razor-thin, because the file's own #2324 finding is that a razor margin destroys the gate's signal and forces each new rule to adjudicate someone else's.

PASS=4 FAIL=0 STATUS=PASS
unconditional_rule_bytes+claude_md=80463  path_scoped_bytes=45435  budget=86000

Note for the maintainer

These are chezmoi sources. Before applying, run chezmoi status ~/.claude and treat every D line as a stop signal — a path-scoped chezmoi diff does not show pending deletions of unmanaged files under an exact_ tree. Then chezmoi apply -v ~/.claude. Nothing here was applied to a live target.


Generated by Claude Code

… and ambient

The 38 rules in exact_dot_claude/rules/ that carry no `paths:` frontmatter load
into every session of every project — 114,284c (~28.6k est. tokens) before the
first prompt is read. This applies the issue's triage: a rule is push (resident,
shaping behaviour unasked), a skill is pull (loads once recognised as relevant),
and the sort is by whether the trigger is recognisable *in advance*.

Nine rules had one, and became stubs:

  read-issue-thread-before-contributing  7431 ->  618  git-plugin:git-issue-scoping
  multi-model-delegation                 6607 ->  756  agent-patterns-plugin (already covered)
  pr-merge-hazards                       7607 -> 2163  git-plugin:git-merge-hazards
  verify-machine-facts-before-publishing 4945 ->  593  documentation-plugin:docs-verify-machine-facts
  taskwarrior-tracking                   4735 ->  620  taskwarrior-plugin:task-add / task-claim
  documentation-authoring                3093 ->  505  documentation-plugin:docs-single-source
  scaffold-fix-backport                  2960 ->  590  code-quality-plugin:code-scaffold-backport
  verify-upstream-before-patching        2710 ->  654  git-plugin:git-upstream-fix-check
  repo-deletion-safety                   2576 ->  649  git-plugin:git-repo-delete-check

tool-use-patterns (9917 -> 7713) was split rather than moved: the always-on
traps stay resident (Read-before-Edit, re-Read after a formatter, `rg -r` is
--replace, a rejected flag looks like no results, parallel-batch cancellation,
Bash denials are terminal) and only the lookup material left, with pointers in
its place.

Every body was moved by script and verified byte-identical against the
pre-change rule; the only in-body edits are sibling-rule path
requalifications. pr-merge-hazards keeps its two gates inline — the
`gh pr list --head` authority order and UNSTABLE != BLOCKED are read at the
instant of deciding, with no earlier moment to invoke a skill.

Rules whose trigger *is* the failure they prevent were deliberately left
resident: diagnose-at-the-failure-point, never-fabricate-test-identifiers,
communication, decision-defaults, security, and above all
skill-and-agent-catalog-check — promoting rules to skills only pays off if the
catalog is consulted at task boundaries, so that one is load-bearing for the
whole plan.

Total: 114,284c -> 76,564c, ~9,430 est. tokens/session, 33% off the surface.

test-claude-context-budget.sh: the 2026-08-09 justification comment claimed a
cleanup had landed that never did (multi-model-delegation was still 6,607
bytes), so a TOTAL_BUDGET_BYTES bump was argued on bytes that were never
freed. Corrected, kept as a lesson, remeasured from disk, and ratcheted
120,000 -> 86,000 — 6.4% headroom, deliberately not razor-thin so the gate
keeps its signal.

Closes #353

Copy link
Copy Markdown
Owner Author

Build (macOS) is red on main, not from this PR

/Users/runner/work/_temp/….sh: line 1: timeout: command not found
##[error]Process completed with exit code 127

timeout is GNU coreutils. macOS runners don't ship it — there is no timeout, and no gtimeout either unless coreutils is brewed. smoke.yml:139 calls it unconditionally.

Evidence this is not mine:

  • The last smoke.yml run on mainfe31477b, "ci(smoke): add a macOS build job alongside Ubuntu (ci(smoke): add a macOS build job alongside Ubuntu #374)", 18:19Z — failed, 44 minutes before this PR was opened. The macOS job has never passed since it was added.
  • This branch changes no workflow and no shell config. git diff --name-only origin/main..HEAD is exact_dot_claude/rules/*, exact_dot_claude/session-plugin.local.md, tests/test-claude-context-budget.sh.
  • smoke.yml is byte-identical to origin/main here.

Not fixing it in this PR — it is unrelated to the rules triage and belongs in its own change. Proposed patch, whichever fits the repo's taste:

Option A — install coreutils in the macOS setup step (keeps the step identical across platforms):

- name: Install coreutils (macOS)
  if: runner.os == 'macOS'
  run: brew install coreutils

…then timeout resolves via the gnubin path, or call gtimeout explicitly.

Option B — drop the coreutils dependency (no install, works on both):

- run: |
    perl -e 'alarm 60; exec @ARGV' zsh -c '
      source ~/.zshrc
      [[ -n "$ZSH_VERSION" ]] || { echo "❌ zsh configuration failed to load"; exit 1; }
      echo "✅ zsh $ZSH_VERSION configuration loaded successfully"
    '

I'd take Bperl is preinstalled on both runners, and it keeps the "60 s is the ceiling" intent that the comment above line 139 spells out without adding a Homebrew step to every macOS run.

Happy to open that as a separate PR if you want it.


Generated by Claude Code

@laurigates
laurigates marked this pull request as ready for review August 20, 2026 03:41
laurigates added a commit that referenced this pull request Aug 20, 2026
…un (#381)

Closes #380. Picks up @claude's patch from [the issue
thread](#380 (comment))
— it did the research and chose option B, but its push was rejected
because the GitHub App lacks `workflows` permission on
`.github/workflows/`. This session has it, so I'm landing the work.

**One substantive change to that patch** — see below; it had a hole that
would have made the smoke test green while testing nothing.

## The fix

`timeout` is GNU coreutils. macOS runners ship neither `timeout` nor
`gtimeout` without `brew install coreutils`, so `Build (macOS)` died at
`command not found` before sourcing anything, and has never passed since
#374 added it.

Replaced with `perl -e 'alarm shift; exec @argv; …' 60 …` in **both**
jobs, not just macOS: perl is preinstalled on both runner images, the
`alarm(2)` timer survives the `exec`, and using one mechanism on both
platforms means the hang-guard can't drift per-platform. That drops the
coreutils assumption rather than papering over it on one side.

## The correction to the proposed patch

The bot's form was `perl -e 'alarm shift; exec @argv' 60 zsh -c '…'`.
perl's `exec()` **returns only on failure**, so with no tail after it
the one-liner falls off the end and exits **0** when the target binary
is missing:

```console
$ timeout 60s definitely-not-a-real-binary; echo $?
timeout: failed to run command 'definitely-not-a-real-binary': No such file or directory
127

$ perl -e 'alarm shift; exec @argv' 60 definitely-not-a-real-binary; echo $?
0          # <- silently green
```

That is not hypothetical here: the Ubuntu job invokes bare `zsh`,
installed by an earlier setup step. Had that install ever broken, this
smoke test would have gone green having sourced nothing — the failure
mode `.claude/rules` calls a lying test. `timeout` reported 127; the
added `warn` + `exit 127` reproduces it.

## Verification

Ran the final form directly — this container has no zsh and no macOS
runner, so these exercise the wrapper's semantics, which is what
changed:

| Case | Result | Want |
|---|---|---|
| success | `0` | 0 |
| `exit 1` | `1` | 1 — real failures must still fail |
| `exit 3` | `3` | exit code passed through, not flattened |
| hang (`sleep 60`, alarm 2) | `142` in 2s | non-zero, at the deadline —
not the 6-hour job limit |
| blocked on stdin (`cat`) | `142` in 2s | the actual hazard the guard
exists for |
| missing binary | `127` | 127 — **was `0`** before the correction |

YAML parses and both `run:` blocks render as intended. `actionlint`
isn't installed in this container, so CI is the first real lint pass —
the `Linters` job covers it.

The issue's acceptance criterion — *"a deliberately hanging `~/.zshrc`
still fails the job rather than running to the 6-hour limit"* — is the
hang and stdin rows above.

## Also

Inline scripts move from double- to single-quoted `zsh -c '…'`, dropping
the `\"`/`\$` escaping so zsh expands `$ZSH_VERSION` itself. Same
output, less quoting to get wrong.

Once this merges, #378 gets its base merged in so its `Build (macOS)`
re-runs against a green base — that's its last red check and the only
thing standing between it and mergeable.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01FQV3do6dGoc7HABQm6v5ZQ)_

Co-authored-by: Claude <noreply@anthropic.com>
@laurigates
laurigates merged commit d0ab77f into main Aug 20, 2026
9 checks passed
@laurigates
laurigates deleted the claude/github-issue-353-9o437n branch August 20, 2026 07:08
laurigates added a commit that referenced this pull request Aug 21, 2026
… a skill (#384)

## What

Three new rules, three extended, and the next step of the skill triage
started in #378 — including the one that keeps the context-budget gate
green.

## Why

**New rules**

- **`gui-companion-cli-not-headless`** — a desktop app that also ships a
`cli` binary invites the assumption that the CLI is standalone. Often it
is a remote control for the running GUI process, so a LaunchAgent or
cron job built on it works whenever a human happens to have the app open
and fails silently the rest of the time. `--help` documents the
commands, never the requirement.
- **`verify-license-position-before-declaring-blocked`** — a `LICENSE`
reads authoritative, but for source-available models the vendor's actual
position routinely lives in a licence FAQ and the discussion tab of the
same repo. Also: HuggingFace's `isOwner` reads `false` for every human
commenter including staff, so check commit authorship instead.
- **`machine-local-notes`** — a `CLAUDE.local.md` is git-ignored, so
nothing reviews it while it loads on every turn. Environment facts stay;
findings belong in the versioned docs.

**Extended rules**

- **`git-hazards`** — the rebase variant of the
clean-merge-broken-result trap. A branch that removes state which
`main`'s new commits depend on replays with *zero conflicts* (different
files, nothing to overlap on) and still produces a tree that cannot run.
A rebase that replays cleanly feels verified, which is why this one is
easy to miss.
- **`never-fabricate-test-identifiers`** — don't fabricate the *subject*
either. A retyped copy of the code under test is not that code; extract
the shipped text and feed it inputs. And when your own target set is
empty, borrow a control set, because an empty run and a broken harness
print the same thing.
- **`shell-pipefail-grep-q`** — additional cases.

## How

**The budget gate is why this PR is shaped this way.**
`tool-use-patterns.md` had reached 18456 bytes against the 10000-byte
per-file cap, and pushed total always-loaded context to 95525 against an
86000 budget — `tests/test-claude-context-budget.sh` refused the commit
outright. `main` sits at 80463, so the new findings were a ~15 KB
overshoot into a file #378 had just dieted.

Rather than raise the cap or drop the findings, this follows the gate's
own remedy and the pattern #378 established: promote the bulky sections
into a skill and leave a stub.

```
tool-use-patterns.md   18456 -> 5317 bytes
always-loaded total    95525 -> 82386  (budget 86000)
per-file cap                    all rules under 10000
STATUS=PASS
```

The seven promoted sections go to
`agent-patterns-plugin:tool-result-traps`
(laurigates/claude-plugins#2472), which needs to land first for the
stub's reference to resolve. They share one law — an empty result, a
green exit, and a well-formed line of output are each claims about
mechanics, not content — so they make one skill rather than several.

The rule keeps its Read / Edit-Write / WebFetch / Bash-denial sections,
plus **one inline line**: control-test any negative that gates an
action. That one stays because it applies to every promoted trap and is
read *while* the decision is being made, so there is no earlier moment
at which to invoke a skill — the same carve-out `pr-merge-hazards.md`
already uses for its two gates.

Also triaged, continuing #378: `ci-cd-multirepo` and
`tool-migration-cutover` reduced to stubs.

**Content preservation** was checked mechanically rather than by eye —
all four extracted blocks (14441 bytes) present verbatim in the skill,
none left behind in the rule, and the original 397 lines tiled with no
gaps so nothing could be dropped *between* blocks. The check asserts a
non-empty input set first, so it cannot pass vacuously.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude documentation Improvements or additions to documentation size/xl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(claude): triage the 37 always-loaded user rules into hooks, skills, and ambient (~10.4k est. tokens/session)

2 participants