Make Claude Code get permanently better at the things you do repeatedly.
Knowledge that costs a session real effort to acquire dies with that session. You and Claude work out a debugging sequence, a deploy-and-verify loop, or a non-obvious API dance; the context window closes; next week a fresh session makes the same mistakes in the same order.
claude-skill-compounder closes that loop. It installs the forging protocol as a skill,
a pool of seed skills that are useful on day one, hooks that keep asking the question, and
a live status-line animation. All of it serves one principle:
Compound improvement. When a procedure is costly to get right and likely to recur, stop re-deriving it and forge it into a reusable skill. Do it adversarially, so the skill actually works for a session that has none of your context.
What is built, what has been shown by running it, and what is still open. Each line carries the command that re-derives it, because every one of these answers moves.
| Area | Where it stands |
|---|---|
| The package | Implemented and in use. There is no runtime service: what ships is the set of skills, hooks, CLIs and the status line that install.sh wires into ~/.claude/ |
| Releases | No tag has been cut yet, so the plain one-liner takes main. git ls-remote --tags https://github.com/ContextLab/claude-skill-compounder.git lists what exists right now |
| CI | .github/workflows/ci.yml runs the suite on ubuntu and macos, shellcheck on both, and claude plugin validate --strict. All five jobs passed on run 33719557434 (2026-09-03), which is the first fully green run on both platforms. Read the current one rather than this line: gh run list --repo ContextLab/claude-skill-compounder --limit 1 |
| End to end | tests/e2e/journey.py walks install, note, reminder, capture, forge, route, apply, report and uninstall against a throwaway config. First real run 2026-09-02: every step PASS, six sonnet calls, 34.9 s, no product failures. Run by hand, never in CI: docs/e2e.md |
| Automatic session review | Ships off, and switching it on spends your quota: What runs against the API. Stage 1 has been paid for six times. Stage 2, which would forge from a CANDIDATE verdict, is off for a structural reason rather than a price: a dispatched forge cannot finish its own routing gate, because claude --version inside one came back "This command requires approval" at the permission layer |
| Usage evidence | One machine. skillreport counts genuine reuse and reports probe and test traffic on a separate line, and on this repository that traffic is most of the total. What each figure is and is not evidence for: docs/measurement.md |
| The two hook thresholds | CI_EDIT_EVERY=12 and CI_PROMPT_COOLDOWN=1200 were picked by judgement, and skillreport needs usage across several repositories before either should move |
Everything known and unresolved, including the parts with no issue open for them, is in
notes/OPEN-THREADS.md.
Skills are Claude Code's mechanism for durable capability, and two things stop them from compounding on their own.
The first is that nothing notices the opportunity. Recognizing that a procedure is worth crystallizing has to happen during the work, because the retrospective where it would otherwise happen is a document nobody writes.
Then there is the skill itself. One written by the session that just solved the problem is usually broken, because its author already knows the answer and quietly assumes context a fresh session will not have. It names a script without saying which directory to run it from. It skips the environment variable that was already exported three hours ago, and it says "fix the error" about an error message that it alone recognizes. The skill reads fine to the person who wrote it and fails six weeks later for everybody else.
This project addresses the first with hooks that keep asking the question, and the second with an adversarial forging protocol built on one idea: the original project is held-out test data. A skill written by the session that needed it is full of references only that session can decode, so exactly one agent β the session itself β is allowed to see the project, and it spends that privilege judging the result rather than writing it. A builder writes the skill in a scratch directory with no path into the project. A separate, cold red-teamer is handed the skill and nothing else, and has to work out from the text alone what the skill is even for. Both run in the background, so none of that traffic lands in the thread you are talking to, and the loop is capped at two rounds: a third has to be earned from a falling count of blocking findings, and the CLI refuses it otherwise.
curl -fsSL https://raw.githubusercontent.com/ContextLab/claude-skill-compounder/main/install.sh | bashPinned to a release, which is the form to prefer once a tag exists:
curl -fsSL https://raw.githubusercontent.com/ContextLab/claude-skill-compounder/v0.3.1/install.sh | SKILL_COMPOUNDER_REF=v0.3.1 bashv0.3.1 is the latest tag (v0.3.0 was cut on 2026-09-03 from a2aa2d4; v0.3.1 fixes
install.sh --update from the managed checkout's own copy);
git ls-remote --tags https://github.com/ContextLab/claude-skill-compounder.git lists the
tags that exist right now. With SKILL_COMPOUNDER_REF unset the installer takes main,
which is whatever was last pushed to it, so two people running the plain one-liner on the
same day can end up on different code. --ref v0.3.1 is the flag form, and over the pipe
it needs bash -s -- --ref v0.3.1.
Or from a clone:
git clone https://github.com/ContextLab/claude-skill-compounder.git
cd claude-skill-compounder && ./install.shRequires python3 (installer only), jq (hooks, CLIs, and status line), and
~/.local/bin on your PATH for the CLIs.
Hooks and skills are picked up without restarting Claude Code, though /hooks forces
a config reload if you want to be certain. Install also appends the three habits to
~/.claude/CLAUDE.md, between a pair of comments that render as nothing, and
--no-doctrine declines that:
what the installer writes into your CLAUDE.md.
The repo is also a valid Claude Code plugin, which gets you the skills and the hooks
without installing anything but not the forge animation:
as a plugin.
Nothing has to be forged for any of this to pay for itself. The two cheap tiers cost one command each, and one more command says whether the install took.
Write a lesson down where a later session will read it:
skillnote add --scope project "the suite is ./run_tests.sh, not pytest" \
--why "pytest collects nothing here"That appends a dated line to this project's .claude/CLAUDE.md, inside a
skillnote:begin/skillnote:end marker block, and writes a note row to the ledger.
--scope global puts it in ~/.claude/CLAUDE.md; --scope memory writes a Claude Code
memory file plus the MEMORY.md index line that gets it read back.
A lesson that applies only at one moment is a reminder rather than a note. Give it the words, the path or the command that should bring it back:
skillnote add --remind --scope project "run the migration before the seed script" \
--keyword migration --command "python manage.py loaddata"hooks/remind.sh states that back when your prompt carries the keyword, or when a Bash,
Write or Edit call matches the command signature. It denies nothing.
skillnote list --scope remind shows what is armed, and skillnote remove <id> disarms
one.
Then check the wiring:
skillforge doctorOne line per check β jq, the state directory, the hook entries in your settings.json,
the status line marker, the skill and CLI symlinks, the ledger, the counters, and any
forge left running β and exit 1 if any of them failed. Run it first whenever something
seems not to be firing.
Forging is the expensive tier and it comes later, once a note has been rewritten often enough to count as a recurrence: Three ways to compound.
| What | What is supported | Where that comes from |
|---|---|---|
| Claude Code CLI | 2.1.241 through 2.1.259 | the range every entry in docs/CLAUDE-CODE-BEHAVIOR.md was measured against: grep -ohE '2\.1\.2[0-9]+' docs/CLAUDE-CODE-BEHAVIOR.md | sort -uV | sed -n '1p;$p' |
bash |
3.2 and newer | macOS ships 3.2 (/bin/bash --version), and the shell rules in docs/DESIGN.md are written against it. The ubuntu runner ships a much newer one; both print theirs in the bash --version | head -1 step of .github/workflows/ci.yml |
zsh |
parsed, not pinned | every shipped script must pass zsh -n as well as bash -n, on both runners, in that same step |
jq |
1.6 and newer | skillforge doctor fails below it and says why: skillforge backfill passes --rawfile, which jq did not have before 1.6 |
python3 |
3.9 | what CI installs (grep python-version .github/workflows/ci.yml). The installer is the only thing here that uses it |
| macOS and Ubuntu | both | the CI matrix (grep -m1 'os: \[' .github/workflows/ci.yml) |
A tag is cut only once the suite is green on both of those operating systems and the end-to-end journey has passed against a throwaway config: docs/e2e.md. The rest of the release procedure is docs/releasing.md.
The curl one-liner clones into ~/.claude/skill-compounder-app and installs from there.
Re-running it re-wires that checkout without moving it. Moving it is a separate ask:
./install.sh --update # fetch and move to SKILL_COMPOUNDER_REF (default main)
./install.sh --update --ref v0.3.1 # or to a tag you name
./install.sh --rollback # back to the ref recorded before that update--update used to be implicit: a re-run ran git pull --ff-only on its way through, so
asking to repair a broken install could hand you a different version of the package than
the one that was broken. The two asks are now separate, and the reasoning is in
docs/DESIGN.md.
install.sh writes the ref it checked out to <state>/install-ref along with the one
before it, and --rollback reads that. With no previous ref recorded it refuses and says
so rather than guessing. SKILL_COMPOUNDER_UPDATE=1 is the environment form of --update,
for the curl pipeline where a flag needs bash -s --.
Those two names are the only environment variables install.sh reads:
SKILL_COMPOUNDER_REF (default main) chooses the ref, and SKILL_COMPOUNDER_UPDATE
(unset by default; 1 turns it on) asks for the move. Both are read at install time only,
so neither belongs in the Tuning table or in ~/.claude/settings.json β set
them on the command that runs the installer, or use --ref and --update.
All three manage only the checkout install.sh cloned. Run them from a clone you made
yourself and they refuse, naming the git commands to run instead: that checkout is
yours, and moving it would discard whatever you had in it.
One part of this package can call the Anthropic API through your own claude CLI
and your own account, and it is off until you switch it on. Everything else here is
shell and jq over files already on your disk, and runs either way. The advertised
install is a curl | bash one-liner, which is why: a command pasted from a web page
should not start spending your quota on its own.
That part is hooks/session-review.sh. It is not wired into settings.json as a hook:
hooks/insight-capture.sh starts it detached on Stop, and only when
SKILL_COMPOUNDER_REVIEW is set to exactly 1 and the session has crossed a
mechanical edit threshold β by default 24 file edits across 8 distinct files
(INSIGHT_AUDIT_MIN_EDITS, INSIGHT_AUDIT_MIN_FILES). It runs one claude -p with no
tools, no MCP servers and no settings sources, asks whether the session that just ended
repeated a procedure worth keeping, writes the answer under
~/.claude/skill-compounder/reviews/, and exits. The answer is VERDICT: NONE or
VERDICT: CANDIDATE <name>, and NONE is the expected one. It forges nothing and
installs nothing.
What leaves the machine. A digest of that one session's transcript: the last 4 MB of
the file (SKILL_COMPOUNDER_REVIEW_TAIL_BYTES), reduced to three kinds of line and then
cut to the last 60 KB of those (SKILL_COMPOUNDER_REVIEW_DIGEST_BYTES). For each Edit,
Write or NotebookEdit: the file path, the first 140 characters of the text replaced,
and the first 140 characters of the replacement. For each Bash call: the first 160
characters of the command. For each block of assistant text: its first 400 characters.
Only non-sidechain assistant records are read, so your own prompts are not copied in
directly, though assistant text can quote them. If the Stop hook wrote a session-audit
record, that goes too: session id, project directory, edit and file counts, and the list
of paths touched. Nothing else is read, and nothing goes anywhere but the API endpoint
your CLI already talks to.
What it costs. Two real runs on sonnet over a 60 KB digest: $0.19 in 60s, and
$0.222 in 80s (2026-08-25, CLI 2.1.245). Six dispatches have since accumulated on one
machine, median $0.17, range $0.042 to $0.222 (as of 2026-09-03). Once you have your own,
skillinsight reviews --all lists them with their prices, and this prints the same three
figures from the index those rows are read out of:
jq -s 'map(.cost_usd|tonumber)|sort|{n:length,min:.[0],max:.[-1],
median:(if length%2==1 then .[length/2|floor]
else (.[length/2-1]+.[length/2])/2 end)}' \
~/.claude/skill-compounder/reviews/index.jsonlA global 21-hour cooldown bounds how often it
can happen at all β 604800 / 75600 = 8 dispatches in any seven-day window, so a ceiling
of $1.52 to $1.78 a week at those two prices. The edit threshold above was measured firing
on 18 of 126 real transcripts spanning 54 days on one machine, and the cooldown collapses
those to 13 distinct days: about 1.7 dispatches a week, or $0.32 to $0.38. Your own rate
depends on how you work. The dispatch is detached and the launch was measured at 3ms, so it adds nothing to
the wall clock of the session that triggers it.
Switching it on. The installer will do it, and prints the data boundary and the measured median cost before it writes anything:
./install.sh --enable-review # or: SKILL_COMPOUNDER_ENABLE_REVIEW=1 ./install.sh
./install.sh --disable-review # and back off againThat writes SKILL_COMPOUNDER_REVIEW=1 into the top-level env block of
~/.claude/settings.json and records in the install manifest that this package set
it, so --disable-review and uninstall only ever remove a value they wrote. A
SKILL_COMPOUNDER_REVIEW you set yourself is left alone and reported, the same
judgement install makes for a doctrine stanza you wrote by hand. Or set it by hand,
beside the hook entries, so both the hook that launches the review and the detached
script that runs it see it:
{"env": {"SKILL_COMPOUNDER_REVIEW": "1"}}Or export it in the shell you start claude from, which turns it on for that terminal
only:
export SKILL_COMPOUNDER_REVIEW=1Only the literal 1 enables it: unset, empty, true and a typo all leave it off, and
the script refuses at its first gate β before it claims the session, takes its lock,
writes a cooldown stamp or reads a byte of any transcript. To turn it back off, set it
to 0 or delete the line. skillforge doctor prints which way it is set, since
nothing else surfaces the answer. For cheaper rather than off,
SKILL_COMPOUNDER_REVIEW_MODEL=haiku was measured at $0.099 against the same digest; it
is not the default because its answer paraphrased the evidence instead of quoting it, and
a NONE you cannot check is not much of a NONE.
| Variable | Default | What it changes |
|---|---|---|
SKILL_COMPOUNDER_REVIEW |
0 |
1 opts in to the paid review; every other value stops the dispatch entirely, from either hook |
SKILL_COMPOUNDER_REVIEW_MODEL |
sonnet |
Model the review runs on |
SKILL_COMPOUNDER_REVIEW_COOLDOWN |
75600 |
Seconds between any two dispatches, across all sessions |
SKILL_COMPOUNDER_REVIEW_FORGE |
0 |
1 lets a CANDIDATE verdict go on to the forging protocol |
SKILL_COMPOUNDER_REVIEW_CLAUDE |
whatever claude resolves to on PATH |
Which CLI to dispatch, when the hook's PATH does not carry one |
Set these in the top-level env block, for the same reason SKILL_COMPOUNDER_STATE
belongs there: both hooks and the dispatched script read them, and
SKILL_COMPOUNDER_REVIEW has a third reader in skillforge doctor, which is what
tells you which way it ended up set.
The second stage, which would take a CANDIDATE and run the full builder/red-team
protocol on it, is off. It was measured once end to end at $3.02 over 19 minutes, two cold
red-team rounds, verdict ABANDONED. Switched on, it writes into reviews/staging/<name>/
and never into ~/.claude/skills, so a forge cannot reach your live config without your
having seen it.
skillreport, skillinsight, skillforge, the status line,
hooks/compound-improvement.sh, hooks/insight-capture.sh, hooks/skill-use.sh and
hooks/claim-gate.sh make no network calls. skillcontrib reaches the network, but only
through gh and only to read: see Contributing a skill back.
Three tiers of durable lesson, and the machinery that feeds them. A note is a dated
line in a CLAUDE.md or a memory file. A reminder is a match rule that a hook states
back at the moment it applies. A skill is the expensive tier, forged through a
builder/red-team loop and installed into ~/.claude/skills/. skillnote writes the first
two in one command each; skillforge drives the third. Nine seed skills ship, so a fresh
install is useful before you have forged anything.
The hooks around those divide into three kinds. Two ask a question and can be read past: one at the start of a substantive turn, one every twelve file edits. Four are gates β the claim gate and the documentation gate refuse outright, the apply gate refuses once per session and then lets go, and the repeat gate can refuse but ships with that arm off. The rest only record: a ledger row per skill invocation, a queue row per candidate a session flags, and a session-audit record for a long session.
| Where to look | What is there |
|---|---|
| docs/architecture.md | What gets installed, the seed pool, the forging protocol and the doctrine it is pinned to, the claim gate, the status line, and what the ledger records |
| docs/operations.md | skillforge doctor and reap, the weekly candidate queue, the state directory and how to recover it, proposing a skill upstream, and the tuning table with every knob, its default and which component reads it |
| docs/measurement.md | What is counted, what each block of skillreport prints, and the limits on every figure here |
| docs/development.md | The suite, the rules it is written under, the end-to-end journey, and releasing |
| docs/DESIGN.md | Why each piece of this package is shaped the way it is |
| docs/CLAUDE-CODE-BEHAVIOR.md | Verified behavior of Claude Code itself, written for anyone building on it and not only for this package |
./uninstall.sh
# or: curl -fsSL https://raw.githubusercontent.com/ContextLab/claude-skill-compounder/main/uninstall.sh | bashRemoves our hooks, leaving other tools' hooks alone, then restores your original status
line and removes the symlinks. Runtime state is left intact; delete it with
rm -rf ~/.claude/skill-compounder.
The curl form works whichever way you installed: with no checkout beside it, the script
reads ~/.claude/skill-compounder/install-manifest.json to find the one you used. If that
checkout has been deleted, clone the repo and run ./uninstall.sh from the clone β the
manifest still identifies the links the old checkout made, so they are removed rather than
disowned.
MIT. See LICENSE.
