Back to the catalog

claude-adapt-rules

Mines agent sessions for the corrections you had to repeat, then distils them into evidence-backed rules — per-repo automatically, global on

Open source Open in the app JSON README (API)

About

Mines agent sessions for the corrections you had to repeat, then distils them into evidence-backed rules — per-repo automatically, global only with your approval.

Details

Kind
Plugins
Topic
No topic detected
Publisher
patrick-de
Origin
gemini
Category
ferramentas
Version
0.1.16
Stars
1
Last push
2026-08-15T20:10:06Z
Repository state
ativo
Language
Python
License
MIT
Added
2026-08-30 14:13:39
Updated
2026-08-30 14:13:39
Origin id
patrick-de/claude-adapt-rules

README

# 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:

```json
{"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:

```bash
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:

```bash
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](#guards-rules-the-machine-can-check).
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:

```bash
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:

```bash
New-Item -ItemType Junction -Path "$env:USERPROFILE\.claude\skills\claude-adapt-rules" -Target "C:\path\to\claude-adapt-rules\skills\claude-adapt-rules"
```

```bash
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.

```bash
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.

```bash
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](#credits).

```bash
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](#credits).

```bash
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.

```bash
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.

```bash
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:

```bash
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.

```json
"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.

```bash
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`:

```json
{ "cleanupPeriodDays": 365 }
```

## Automation

- **SessionStart hook** (`bin/inject.py`) — puts the current project's rules into context.
- **SessionEnd hook** (`bin/capture.py`, or `bin/capture.sh` / `bin/capture.ps1` as 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](#guards-rules-the-machine-can-check).
- **Weekly refresh** — `hooks/weekly_extract.ps1` (Windows Task Scheduler) or
  `hooks/weekly_extract.sh` (cron). Both re-extract full history and then archive.

```bash
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"
```

```bash
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?

```bash
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:

```bash
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:

```bash
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?

```bash
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`](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`](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](https://github.com/rebelytics/one-skill-to-rule-them-all)**
by **Eoghan Henn** ([rebelytics](https://rebelytics.com)), 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](LICENSE).

More