claude-adapt-rules
Mine your own Claude Code sessions for the moments you corrected the agent, distil those into rules, and land them where a future session will actually read them.
Two tiers, because the cost of a rule is not the same everywhere:
| Tier | Target | Policy |
|---|---|---|
| repo | ~/.claude-adapt-rules/rules/repos/<project>/rules.md | auto-written; blast radius is one project, and it's a git diff away from gone |
| global | ~/.claude-adapt-rules/rules/global/PROPOSED.md → ~/.claude/CLAUDE.md | proposed only, you approve; every line is loaded in every session of every project |
Where the human text actually is
The interesting finding from building this. In a Claude Code desktop install,
~/.claude/projects/<slug>/<session>.jsonl records your prompts as:
{"type": "queue-operation", "operation": "enqueue", "content": "<what you typed>"}
The type: "user" records are almost entirely tool results and machine continuations.
On this machine: 17k type:user records, of which 64 survive noise filtering — and
all 64 are machine-generated (Continue from where you left off., compaction summaries,
skill payloads). The 1325 real prompts are all in queue-operation records.
A miner that reads type: "user" learns nothing about the user. Both channels are read
here, queue preferred, overlaps de-duplicated.
Install
As a Claude Code plugin:
claude plugin marketplace add https://github.com/Patrick-DE/claude-adapt-rules.git
Or from a local checkout, which is what you want while iterating on the tool itself:
claude plugin marketplace add /path/to/claude-adapt-rules
Then enable claude-adapt-rules. That registers two hooks — SessionStart (inject this
project's rules) and SessionEnd (capture corrections) — plus the /claude-adapt-rules skill.
A third, opt-in PreToolUse hook is described under Guards.
Requires Python ≥ 3.12 on PATH as python.
| Platform | What loads | Notes |
|---|---|---|
| Claude Code (Windows) | skill + both hooks | primary target; hooks exec python directly, no shell needed |
| Claude Code (macOS/Linux) | skill + both hooks | change command to python3 in .claude-plugin/plugin.json if python is absent |
| Antigravity / Gemini | skill + GEMINI.md context | no session hooks — run extract on a schedule and read rules from ~/.claude-adapt-rules/ |
| Codex | skill + AGENTS.md context | same |
State lives in ~/.claude-adapt-rules/ (CLAUDE_ADAPT_RULES_HOME overrides), never inside the
plugin directory — installed plugins live under a versioned cache path, so an update
would orphan your ledger, queue and archive. CLAUDE_ADAPT_RULES_USER_HOME separately overrides
the OS home directory itself — only used for the pre-rename ~/.claude-learn lookup below.
~/.claude-adapt-rules/
rules/ledger.json rule identity, evidence, adoption dates, violations
rules/global/PROPOSED.md awaiting your approval
rules/repos/<project>/ auto-written per-project rules
rules/candidates/ distilled candidate batches
data/corpus, queue, archive, reports
Run the CLI from anywhere without installing the package:
bin/claude-adapt-rules.sh status # or bin\claude-adapt-rules.ps1 status on Windows
Upgrading from claude-learn
The state root is derived from the tool's own name, so the rename would otherwise
orphan everything you had: the ledger, adopted globals, repo rule files, the archive of
cited transcripts, and the consumed-event markers. A fresh ingest would then restart ids
at R-0001 against a CLAUDE.md that already cited them.
Nothing to run. On first use, ~/.claude-learn/ is adopted automatically — copied, never
moved, so the old root survives as a rollback and is marked as read. Files the new root
already has are left alone, and the two queues are merged by (session, record) because
neither side is authoritative: one holds everything captured before the rename, the other
everything after. The old <!-- claude-learn --> block in ~/.claude/CLAUDE.md is
replaced rather than appended to, so pre-rename rules stop being loaded twice.
Using the skill without installing the plugin
Plugin skills only load once the plugin is installed, and .claude/skills/ only loads
inside its own project. To get /claude-adapt-rules in every project from a plain checkout, link
it into your user skills directory — no admin needed on Windows, and it stays a single
source of truth:
New-Item -ItemType Junction -Path "$env:USERPROFILE\.claude\skills\claude-adapt-rules" -Target "C:\path\to\claude-adapt-rules\skills\claude-adapt-rules"
ln -s /path/to/claude-adapt-rules/skills/claude-adapt-rules ~/.claude/skills/claude-adapt-rules
Skills are enumerated at session start, so it appears in the next session. Remove the link if you later install the plugin, or the same skill loads twice.
How rules reach a session
Distilling rules is worthless if nothing reads them. Both tiers have a delivery path:
| Tier | Delivery |
|---|---|
| repo | a SessionStart hook injects the current project's rules as session context — nothing is written into your other repositories, so teammates see no diff and a reworded rule takes effect next session |
| global | adopt --apply-global splices a marked block into ~/.claude/CLAUDE.md after you name the ids |
Sessions started inside a git worktree receive the parent repository's rules. Projects with no rules get nothing — the hook prints nothing and exits 0.
claude-adapt-rules doctor # is any of this actually working?
doctor exists because hooks fail open: a broken capture is silent by design. It reports
captured/pending events, recent hook failures, archive coverage, transcripts approaching
the cleanup age, and how many rules the current project would receive.
Pipeline
transcripts → signals → extract → /claude-adapt-rules → ledger → render
(parse) (score) (bundles) (the only (identity, (two tiers)
model step) rot tracking)
Everything except /claude-adapt-rules is deterministic and stdlib-only — the SessionEnd
hook imports this package on every session exit, so a dependency here would break
unrelated work in other projects.
python -m claude_adapt_rules.cli status # parse and report, write nothing
python -m claude_adapt_rules.cli extract # corpus + per-project bundles
python -m claude_adapt_rules.cli ingest ~/.claude-adapt-rules/rules/candidates/<date>.json
python -m claude_adapt_rules.cli verify # every quote must be verbatim
python -m claude_adapt_rules.cli adopt R-0001 --apply-global
python -m claude_adapt_rules.cli rot # which rules aren't working
python -m claude_adapt_rules.cli guards # which ones a hook could enforce
python -m claude_adapt_rules.cli workflows # work repeated by hand
python -m claude_adapt_rules.cli constraints # rules for what you write next
python -m claude_adapt_rules.cli doctor # is any of it reaching a session
python -m claude_adapt_rules.cli register --system # OS-level weekly schedule (see below)
Then in Claude Code: /claude-adapt-rules reads the bundles and writes the candidates file.
What makes an event worth reading
Lexical signals (don't, wrong, always, nicht, warum hast du) are cheap and noisy.
Structural signals are weighted higher because they're harder to fake:
- repeated_instruction — you said the same thing twice in one session (strongest)
- user_denied — you rejected a tool call outright
- interrupted — you hit escape
after_edit is deliberately worth zero. 407 of 778 prompts follow an edit; scoring it
ranks "commit and push" alongside a real correction. It only adds a point when the words
are corrective too.
Work you repeat by hand
Every signal above is corrective. That structurally cannot find work you drive by hand five times without ever complaining — there is no signal to find. Idea credited to Task-Observer.
claude-adapt-rules workflows
Counts recurring tool sequences across sessions, excluding any span the user corrected, denied or interrupted — those are already covered above, and proposing a skill for work that went wrong is backwards.
A candidate must reach beyond the ordinary edit loop. Measured here before that filter
existed, the top result was Read → Edit → Bash, seven times across four projects: that
is what coding is, and it buried everything distinctive. A sequence made only of
Read/Edit/Write/Bash/Grep/Glob is therefore dropped. With the filter, the same
corpus reports two candidates instead of eleven.
Output is candidates, not conclusions — a repeated shape of work is worth looking at, not proof a skill is warranted.
Rules for the next thing you write
Rules reach sessions. Nothing reached authoring, so a new skill or agent file gets written without the constraints its author already established, and the same correction gets learned again through the new artifact. Idea credited to Task-Observer.
claude-adapt-rules constraints # current project + globals
claude-adapt-rules constraints --project app --out CONSTRAINTS.md
Adopted rules only, globals first, as a block to paste into whatever you are writing. It prints rather than editing your files: writing into someone's skill file uninvited is the behaviour this project exists to correct.
Scope comes from generality, not from frequency
Every rule is classified applies: universal | project. Universal means it would hold in
a repo you have never seen — "never commit code that does not build" qualifies after being
said once, which no evidence-count gate would ever promote. Project means it is tied
to this codebase's tooling, architecture or vocabulary.
A universal claim is vetoed when the rule text names a path, filename, identifier or
known project name, and the reason is reported:
? R-0027 is universal but names a path (releases/canvas-debug.log) — scoped to repo
project is never widened. Unclassified rules fall back to the old count gate
(≥2 projects or ≥3 sessions), which is only a proxy for generality.
claude-adapt-rules reclassify R-0024=universal R-0026=project --apply
Promotion out of repo scope drops the rule back to proposed: repo rules auto-apply, global rules never do.
Worktree slugs (...-app--claude-worktrees-brave-newton-a1b2c3) collapse onto their
repository — otherwise one repo's quirk looks like cross-project evidence and gets promoted.
Why it compounds
ingest treats a candidate matching an already-adopted rule as a violation, not a new
rule. That is the signal worth having: the rule existed and did not work. Reword it, hoist
it earlier, or convert it to a hook.
rot then splits adopted rules into still being broken (escalate) and quiet for 30 days
(stop paying its token cost).
Three delivery tiers, not two
A rule used to be always-on or nothing, so ~/.claude/CLAUDE.md could only grow —
guards were the sole exit, and they only take the subset a regex can decide.
claude-adapt-rules defer R-0008 --trigger "building or restyling a user interface"
claude-adapt-rules defer R-0008 --promote # bring it back
The rule stays adopted and leaves the always-on block. What remains there is one line
naming the triggers and pointing at rules/global/ON-DEMAND.md, which holds the rules in
full, grouped by trigger.
The trigger is mandatory. A deferred rule with no stated condition is one nothing will ever read, which is strictly worse than retiring it — it still looks live in the ledger.
delivery is orthogonal to scope: scope says where a rule applies, delivery says
how it arrives. Both a global and a repo rule can be always-on or on-demand.
Guards: rules the machine can check
A rule in CLAUDE.md is a suggestion the model weighs against everything else in context.
For the subset a regex can decide — --no-verify, a banned import, a forbidden command —
weighing is the wrong mechanism: a PreToolUse hook simply refuses the call.
Those rules are already flagged enforceable. A guard is the check itself:
claude-adapt-rules guards # enforced by a hook, vs still only prose
claude-adapt-rules guards --set R-0024 --tool Bash \
--pattern=--no-verify --message='run the build and suite instead'
Use --pattern= with an =, not a space — the patterns worth guarding are usually flags,
and argparse would read a leading - as an option.
Enable it by adding the hook. It is opt-in and scoped to one tool on purpose: the
script costs ~209 ms per call, and gating every Read and Grep to catch one flag is a
bad trade.
"PreToolUse": [
{ "matcher": "Bash",
"hooks": [ { "type": "command", "command": "python",
"args": ["/path/to/claude-adapt-rules/bin/guard.py"], "timeout": 10 } ] }
]
Guards are read from the ledger at hook time rather than compiled into a generated script. A generated script goes stale the moment a rule is reworded or retired, and a stale gate that refuses a legitimate command is worse than no gate.
Only adopted rules enforce, and each tool declares which input field a guard reads, so
a pattern cannot fire on an unrelated path in the same call. guard.py sits in front of
every matched tool call, so it fails open and logs — the one place here where that is
correct. The loud path is --set, which refuses to store a pattern it cannot compile.
Known limitation: a guard matches command text and cannot tell running a flag from
mentioning it, so a command quoting the guarded string is refused. Inherent to the
mechanism. guards --clear R-0024 disarms without touching settings.
The escalation ladder this completes: prose → still violated after adoption (rot) →
reword or hoist it earlier → if a regex can decide it, make it a guard and drop it from
CLAUDE.md. That last step is the only thing that stops the always-on block growing
forever.
Evidence integrity
verify re-checks every quote against the decoded transcript text and fails on
paraphrase, changed capitalisation, or attribution to the wrong session. Raw JSONL escapes
inner quotes, so grepping file bytes gives false failures — hence decoded comparison.
The first real run produced two bad quotes out of 48, both mine, both caught this way.
Transcripts expire — archive or the audit trail rots
Claude Code deletes transcripts after cleanupPeriodDays (default 30). Measured
2026-07-26: the oldest file in ~/.claude/projects was exactly 30 days old, and four
evidence quotes from rules distilled that same morning already cited deleted sessions.
python -m claude_adapt_rules.cli archive # cited sessions only
python -m claude_adapt_rules.cli archive --all # every session, before it ages out
The weekly job archives after every extract. verify reads the archive too, and reports a
vanished transcript as expired rather than as bad evidence — decay must not look like
fabrication.
To keep raw history longer, raise retention in ~/.claude/settings.json:
{ "cleanupPeriodDays": 365 }
Automation
- SessionStart hook (
bin/inject.py) — puts the current project's rules into context. - SessionEnd hook (
bin/capture.py, orbin/capture.sh/bin/capture.ps1as shims) appends each finished session's candidates to~/.claude-adapt-rules/data/queue/queue.jsonl. No model, no network, always exits 0.
Both are declared by the plugin and exec python directly, so neither needs a shell — on
Windows that removes the Git Bash dependency. Where only python3 exists, change the
command in .claude-plugin/plugin.json.
- PreToolUse hook (
bin/guard.py) — refuses a call that breaks a guarded rule. Not declared by the plugin: a hook that blocks tool calls is opt-in, and you add it yourself. See Guards. - Weekly refresh —
hooks/weekly_extract.ps1(Windows Task Scheduler) orhooks/weekly_extract.sh(cron). Both re-extract full history and then archive.
schtasks /Create /TN "claude-adapt-rules weekly" /SC WEEKLY /D MON /ST 09:00 /TR "powershell -NoProfile -ExecutionPolicy Bypass -File C:\path\to\claude-adapt-rules\hooks\weekly_extract.ps1"
0 9 * * 1 /path/to/claude-adapt-rules/hooks/weekly_extract.sh
The distil step stays manual: it needs a model. Run /claude-adapt-rules when the bundles look
worth reading.
Cadence
Capture is automatic and distillation is not, so the queue grows quietly until someone
remembers it. Pick a rhythm and let doctor police it — it reports the age of the
oldest undistilled event, not just the count, and flags anything left longer than
--stale-days (default 7):
pending distillation ....... 6
oldest pending ............. 2d (2026-08-04)
Weekly suits a single developer. Task-Observer's author runs reviews three mornings a week and reports it scales better as the library grows.
Scheduling the weekly pass
extract → archive → distil → ingest is easy to run once and then forget. Two ways to
put it on a schedule instead of relying on memory:
Native routine (recommended)
/claude-adapt-rules:register
Registers a Claude Code scheduled routine (claude-adapt-rules-weekly) that re-extracts,
archives, distils the pending slice, checks candidates, and ingests — unattended, once a
week. It only runs while Claude Code is open (or catches up at the next launch), so click
Run now once after registering to pre-approve the tool calls it would otherwise have to
ask for unattended. It never runs adopt --apply-global: global rules always wait for you.
/claude-adapt-rules:unregister
removes it. Both commands are idempotent — running register twice reports the existing
routine instead of creating a second one.
OS-level fallback
No Claude Code desktop app, or want the job to run even while it is closed?
claude-adapt-rules register --system # Mondays 09:00, distil on
claude-adapt-rules register --system --day TUE --time 14:00 --no-distil
claude-adapt-rules unregister --system
Installs a Windows Task Scheduler entry or a crontab line (marker-commented, so
re-registering replaces it cleanly without touching anything else in your crontab) that
resolves this plugin's current install path from installed_plugins.json on every run —
a later plugin update cannot break it. This path only runs extract + archive, plus a
headless distil-and-gate step unless --no-distil is passed; it still never ingests or
adopts anything unattended.
Without --system, register/unregister cannot reach the native scheduler — this CLI has
no way to call it — and just point back at the slash commands above.
Layout
src/claude_adapt_rules/
transcripts.py locate and parse Claude Code session transcripts (stdlib only)
signals.py score human prompts by correction signal, lexical + structural
extract.py turn transcripts into a scored corpus + per-project evidence bundles
candidates.py validate a distilled candidates file before it nears the ledger
classify.py judge a rule universal vs project; veto misclassified paths
ledger.py rule identity, provenance, scope promotion, rot tracking
render.py render the ledger into the two delivery tiers (repo + global)
verify.py check every rule's evidence is verbatim in its transcript
archive.py copy cited transcripts out of the 30-day cleanup path
inject.py put a project's rules into context at SessionStart
guards.py PreToolUse enforcement for the subset a regex can decide
authoring.py put adopted rules in front of the next skill/agent file written
workflows.py find work repeated by hand that no correction signal reveals
harness.py inventory which skills, agents and tools ever actually fire
impact.py correction-rate density before vs after a rule's adoption
migrate.py one-time adoption of state written under the tool's earlier name
doctor.py health-check logic: is capture -> distil -> delivery working?
paths.py where this tool's state and Claude Code's own config each live
jsonl.py tolerant JSONL line reading, shared by every transcript consumer
atomic.py whole-file writes that can't leave a half-written file behind
cli/ the CLI: argparse wiring plus the pipeline/ledger/report command modules
skills/claude-adapt-rules/ the model-facing distillation instructions
bin/ hook entry points (capture, inject, guard) + CLI wrappers
hooks/ weekly extract for Task Scheduler (.ps1) and cron (.sh)
.claude-plugin/ Claude Code plugin + marketplace manifests
.codex-plugin/ Codex manifest; AGENTS.md is its context file
gemini-extension.json Antigravity / Gemini manifest; GEMINI.md is its context file
tests/ suite run with `python -m pytest`
No rules ship with the plugin — the ledger starts empty and everything you distil stays
in ~/.claude-adapt-rules/.
Closing the loop
Capture was automatic; everything after it was not. The weekly job can now draft candidates unattended:
CLAUDE_ADAPT_RULES_DISTIL=1 # opt-in, needs the `claude` CLI on PATH
It drafts and stops. Ingest stays manual: a bad rule reaches every session of every project, and global text waits for a human yes. What replaces the human reader is a mechanical gate:
claude-adapt-rules check-candidates <file> --write-accepted <file>
Every quote must be verbatim in the session it cites, or the candidate is dropped. This
is stricter than verify: an expired transcript rejects a candidate rather than passing,
because there is no rule to protect yet and admitting an uncheckable quote is how
unverifiable rules are born.
Is any of it working?
claude-adapt-rules impact # correction rate before vs after adoption, per project
claude-adapt-rules rot # broken-and-caught vs broken-and-shipped
claude-adapt-rules harness # which skills, agents and tools ever fire
impact is built to refuse to conclude. It reports per project rather than pooling,
always prints the sample size, and says "no conclusion" under 50 prompts a side. On the
real corpus every window currently refuses — including a 100% → 20% swing on n=2, which
is precisely the reading the refusal exists to prevent. A number that looks like a verdict
gets read as one.
rot now leads with guard fires, because a block is the one signal available without a
distillation run: it separates broken and caught from broken and shipped.
Where this is going
docs/vision.md states the goal — every correction costs the user
once — the principles each defect in this repo paid for, and the five places the
system still falls short. docs/roadmap.md turns those into a
checklist with acceptance tests.
The shortest summary of the gap: capture is automatic, everything after it is not.
Credits
Three features here came from reading Task-Observer — One Skill to Rule Them All by Eoghan Henn (rebelytics), licensed CC BY 4.0:
| borrowed | where it lives here |
|---|---|
| coverage gaps as a first-class category, not just corrections | workflows |
| cross-cutting principles applied when artifacts are written | constraints |
| a standing review cadence rather than ad-hoc distillation | doctor --stale-days |
The two projects solve adjacent problems and are worth reading together. Task-Observer improves skills — the procedures — by observing live in every session, and works anywhere Claude runs, including web and mobile. This project distils rules — the constraints — by mining stored transcripts after the fact, which buys a verbatim evidence chain and rule identity at the cost of needing transcripts on disk. Only the ideas above were taken; no text or code was copied.
Author
Built by Patrick Eisenschmidt — https://github.com/Patrick-DE/claude-adapt-rules.
License
MIT. See LICENSE.