# CLAUDE.md Harvester

The reader has been told to "scroll back through your last week of sessions and find every place you
typed a correction." Nobody does that. It is hours of scrollback across sessions that have already
been cleared, and the corrections that matter most are the ones they typed *weeks* ago and have since
forgotten they were ever angry about.

**You are going to do it for them, from disk, in one pass.**

Every correction they have ever typed is already sitting on their machine in JSON. A rule they typed
twice is a rule they have paid for twice. That is the gold, and it is mechanically findable.

## The data — verified on disk, do not guess this

Claude Code writes one JSONL file per session under:

```
~/.claude/projects/<escaped-project-path>/<session-uuid>.jsonl
```

The directory name is the project's absolute cwd with **every non-alphanumeric character replaced by
`-`**. `C:\new\University` becomes `C--new-University`. `/home/me/api` becomes `-home-me-api`.

One JSON object per line. The line shapes you care about:

| `type` | What it is |
|---|---|
| `user` **with** a `toolUseResult` key | **A TOOL RESULT, NOT A HUMAN.** `content` is `[{type:"tool_result"}]`. |
| `user` with `origin.kind === "human"` | **An actual person typing.** `content` is a plain string. |
| `user` with `origin.kind === "task-notification"` | A background-agent notification. Not a human. |
| `assistant` | `message.content[]` of `thinking` / `text` / `tool_use` blocks. |
| `mode`, `ai-title`, `attachment`, `file-history-snapshot`, … | Bookkeeping. Ignore. |

**This is the whole trick, and getting it wrong makes the skill worthless.** In a real 1,752-line
session measured on a working machine, there were 360 lines of `type: "user"` — and **317 of them
were tool results, not the human.** A naive `grep '"type":"user"'` returns ~88% noise, and the noise
is full of the words you are searching for, because tool results echo the file contents back.

The filter that isolates a human turn:

```
o.type === 'user'
  && o.toolUseResult === undefined   // not a tool result
  && o.origin?.kind === 'human'      // not a task-notification
  && !o.isMeta && !o.isSidechain     // not injected context, not a subagent
  && !text.startsWith('<command-name>')  // not a slash command like /clear
```

The same directory also holds `memory/MEMORY.md` — Claude Code's **auto memory**, notes it wrote
*itself* from the user's corrections ([docs](https://code.claude.com/docs/en/memory)). Read it in
Step 4. A rule already captured there is not a new finding.

## Step 0 — Preflight

`node` is the only tool you can count on: it ships with Claude Code. **`rg` and `jq` are frequently
absent** — on the reference machine for this skill, neither was on `PATH`. Check, then choose:

```bash
node --version
command -v rg jq || echo "no rg/jq — node does the mining (this is the normal case)"
```

Locate the transcripts. Derive the directory, do not guess it:

```bash
node -e "const{homedir}=require('os'),{join}=require('path'),fs=require('fs');
const d=join(homedir(),'.claude','projects',process.cwd().replace(/[^a-zA-Z0-9]/g,'-'));
console.log(d, fs.existsSync(d)?'OK':'MISSING');
if(fs.existsSync(d))console.log(fs.readdirSync(d).filter(f=>f.endsWith('.jsonl')).length,'sessions');"
```

If it prints `MISSING`, the user has run Claude Code from a *different* directory than the repo root
(a subdirectory, or a different drive letter casing). List `~/.claude/projects/` and match by eye —
do not silently mine the wrong project.

## Step 1 — Mine the corrections

Do **not** try to inline this as a shell one-liner. Backslashes in the regexes get eaten by heredocs
and by PowerShell quoting — this bit the author of this skill on the first attempt. **Write the
script to a file with the Write tool, then run it.**

Write `.claude/scratch/harvest.mjs`:

```js
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';

const dir = join(homedir(), '.claude', 'projects', process.cwd().replace(/[^a-zA-Z0-9]/g, '-'));
if (!existsSync(dir)) { console.error('no transcripts at', dir); process.exit(1); }

// A correction is the user telling the agent it did the wrong thing.
const CORRECTION = [
  /^(no|nope|nah)\b/i,                    // flat contradiction
  /^(stop|wait|hold on)\b/i,              // interrupt
  /\b(don'?t|do not|never)\s+\w+/i,       // prohibition
  /\b(that'?s|thats|it'?s)\s+(wrong|not right|incorrect|broken)\b/i,
  /\b(i (said|told you|already told you)|as i said)\b/i,
  /\bwhy (did|are) you\b/i,               // exasperation = a rule you never wrote down
  /\b(revert|undo|roll ?back|put it back)\b/i,
  /\bwe (use|don'?t use|never use|always use)\b/i,   // convention statement
  /\buse\s+\S+\s+not\s+\S+/i,             // "use pnpm not npm"
  /\b(actually|instead)\b.{0,40}\b(use|should|need)\b/i,
];

const rows = [];
for (const f of readdirSync(dir).filter((f) => f.endsWith('.jsonl'))) {
  let prevTool = null, prevFile = null;
  for (const line of readFileSync(join(dir, f), 'utf8').split('\n')) {
    if (!line) continue;
    let o; try { o = JSON.parse(line); } catch { continue; }

    if (o.type === 'assistant') {
      const tu = (o.message?.content || []).filter((b) => b.type === 'tool_use');
      if (tu.length) {
        const last = tu[tu.length - 1];
        prevTool = last.name;
        prevFile = last.input?.file_path || last.input?.command || null;
      }
      continue;
    }
    if (o.type !== 'user') continue;
    if (o.toolUseResult !== undefined) continue;    // TOOL RESULT — not a human
    if (o.isMeta || o.isSidechain) continue;
    if (o.origin?.kind !== 'human') continue;       // task-notifications are not humans

    const c = o.message?.content;
    const text = typeof c === 'string' ? c
      : Array.isArray(c) ? c.filter((b) => b.type === 'text').map((b) => b.text).join('\n') : '';
    if (!text || text.startsWith('<command-name>')) continue;

    const head = text.trim().split('\n').filter(Boolean)[0] || '';
    const hit = CORRECTION.find((r) => r.test(head));
    if (!hit) continue;

    rows.push({
      session: f.slice(0, 8),
      ts: o.timestamp,
      branch: o.gitBranch || null,
      after_tool: prevTool,          // a correction right after a Write/Edit reverts that Write/Edit
      after_target: prevFile ? String(prevFile).slice(0, 80) : null,
      pattern: String(hit),
      text: head.slice(0, 300),
    });
  }
}
rows.sort((a, b) => String(a.ts).localeCompare(String(b.ts)));
console.log(JSON.stringify(rows, null, 2));
```

Run it and keep the JSON:

```bash
mkdir -p .claude/scratch
node .claude/scratch/harvest.mjs > .claude/scratch/corrections.json
node -e "const r=require('./.claude/scratch/corrections.json');
console.log(r.length,'corrections across',new Set(r.map(x=>x.session)).size,'sessions');
const t={}; for(const x of r) t[x.after_tool||'(none)']=(t[x.after_tool||'(none)']||0)+1;
console.log('corrections by the tool call they followed:',t);"
```

**Corrections that land immediately after a `Write` or `Edit` are the highest-value rows in the
file.** The user is reverting something the agent just did. `after_target` tells you *which file* —
that is the rule's scope, handed to you.

If the harvest comes back with fewer than ~5 rows, widen: drop the `^` anchors and scan the whole
turn body rather than its first line. A user who phrases corrections politely ("could we not put
that there") will not trip a `^no\b`.

## Step 2 — Cluster into candidate rules

Read `corrections.json` yourself. This is the judgment step and it does not belong in a regex — the
script found the *turns*, you extract the *rule*.

For each cluster:

1. **Group by the thing being corrected**, not by wording. "no, we use pnpm", "stop using npm", and
   "why did you run npm install" are **one rule with a recurrence of 3**.
2. **Count the recurrence.** Recurrence is the entire ranking signal. The docs give the trigger
   verbatim: add to CLAUDE.md when "Claude makes the same mistake a second time" or "You type the
   same correction or clarification into chat that you typed last session"
   ([docs](https://code.claude.com/docs/en/memory)). **Recurrence ≥ 2 is a rule. Recurrence 1 is a
   candidate, and you mark it as such.**
3. **Recover the failure it prevents.** Read the surrounding turns in the transcript — you have the
   session file and the timestamp. What did the agent actually do? A rule whose failure is not
   written next to it gets deleted by a future reader who no longer remembers why it is there, and
   then the bug comes back.
4. **Write it specific and checkable.** The docs' own examples: "Use 2-space indentation" not "Format
   code properly"; "Run `npm test` before committing" not "Test your changes"; "API handlers live in
   `src/api/handlers/`" not "Keep files organized" ([docs](https://code.claude.com/docs/en/memory)).
   If you cannot tell by looking at the code whether the rule was followed, it is not a rule.

**Discard anything the agent would have done anyway.** "Write clean code" was never a correction; it
was never going to be. If the line does not prevent a specific failure that actually occurred in the
transcript, it costs context on every session forever and buys nothing.

## Step 3 — Sort each rule into its correct home

This is the step that makes the output worth accepting. **Not every correction belongs in
`CLAUDE.md`,** and proposing one that doesn't is how the file rots.

CLAUDE.md is **context, not enforcement**. The docs are blunt: both memory systems are treated "as
context, not enforced configuration," and there is "no guarantee of strict compliance"
([docs](https://code.claude.com/docs/en/memory)). So:

| The correction is… | It belongs in | Why |
|---|---|---|
| A fact or convention that shapes behaviour ("we use pnpm", "handlers live in `src/api/`") | **CLAUDE.md** | This is exactly what it is for. |
| Something that **must** hold every time — a command that has to run before every commit, a path that must never be written | **A `PreToolUse` hook** in `settings.json` | Only a hook can *block*. Asking nicely in markdown is not a control. |
| A multi-step procedure rather than a fact | **A skill** (`.claude/skills/<n>/SKILL.md`) | The docs draw the line here: write a skill "when a section of CLAUDE.md has grown into a procedure rather than a fact" ([docs](https://code.claude.com/docs/en/skills)). |
| Only relevant to some files | **A path-scoped rule** (`.claude/rules/*.md` with `paths:`) | Loads only when those files are touched. Costs nothing otherwise. |

For every rule you route to a hook, say so **in the output** and give the exit code, because this is
the single most common way an enforcement rule silently fails:

> Claude Code treats **exit code 1 as a non-blocking error and proceeds with the action**, even
> though 1 is the conventional Unix failure code. **If your hook is meant to enforce a policy, use
> `exit 2`.** ([hooks docs](https://code.claude.com/docs/en/hooks))

A "rule" that is really a hook, left in CLAUDE.md, will be followed most of the time and fail exactly
when it matters.

## Step 4 — Check against what already exists

Do not propose what is already there. Read all three:

```bash
node -e "const fs=require('fs');for(const p of ['CLAUDE.md','.claude/CLAUDE.md','CLAUDE.local.md'])
  if(fs.existsSync(p))console.log('---',p,fs.readFileSync(p,'utf8').split('\n').length,'lines');"
ls .claude/rules/ 2>/dev/null
```

```bash
# Claude Code's AUTO MEMORY — notes it already wrote itself from these same corrections.
node -e "const{homedir}=require('os'),{join}=require('path'),fs=require('fs');
const m=join(homedir(),'.claude','projects',process.cwd().replace(/[^a-zA-Z0-9]/g,'-'),'memory','MEMORY.md');
console.log(fs.existsSync(m)?fs.readFileSync(m,'utf8'):'(no auto memory)');"
```

Mark every candidate `NEW`, `DUPLICATE` (already stated — drop it), or `CONFLICTS` (contradicts an
existing line — flag it loudly; the docs warn that when two rules disagree "Claude may pick one
arbitrarily").

Then check the **budget**. The docs: "target under 200 lines per CLAUDE.md file. Longer files consume
more context and reduce adherence" ([docs](https://code.claude.com/docs/en/memory)). Count the
current file, add your proposals, and if the total crosses 200 you must say what to **cut** — push
path-specific material into `.claude/rules/`. A harvest that only ever adds is a harvest that
degrades the file it is trying to improve.

## Step 5 — Emit the proposal

**Write `CLAUDE.md.candidates.md` in the repo root. Never touch `CLAUDE.md` itself.** These are rules
mined from a machine's history; a human accepts or rejects each one. Propose, never apply.

The report file contains, with real counts and real quotes:

```markdown
# CLAUDE.md candidates — harvested <date>
Mined N sessions · M corrections · K clustered rules.
Current CLAUDE.md: X lines. Proposed: +Y. Target: under 200.

## Accept these first — you have paid for them more than once

### 1. Use pnpm, never npm  ·  recurrence 4  ·  NEW
> "no, we use pnpm"            — session 31478d1f, 2026-07-02, after Bash(npm install)
> "stop. pnpm."                — session be94abf4, 2026-07-09, after Bash(npm ci)
**Failure it prevents:** the agent runs `npm install`, rewrites `package-lock.json`, and the
lockfile fights `pnpm-lock.yaml` in CI.
**Proposed line:** `- Package manager is pnpm. Never run npm or yarn; the lockfile is pnpm-lock.yaml.`

### 2. Never edit files under `generated/`  ·  recurrence 2  ·  NEW
### 2. Run the formatter before every commit · recurrence 3 · ALREADY COVERED

## Single sightings — park these, do not add yet
| Rule | Seen | Why it is not a rule yet |
|---|---|---|
| ... | 1× | One correction is a preference. Two is a rule you have paid for twice. |

## These are NOT CLAUDE.md rules — route them here instead
| Correction | Belongs in | Why |
|---|---|---|
| "always run the tests before you commit" | `PreToolUse` hook, **exit 2** | Must hold every time. Markdown cannot block; a hook can. |
| "here's how we cut a release" (6 steps) | a skill | It is a procedure, not a fact. |

## Conflicts with your existing CLAUDE.md
| Line X says | A correction says | Resolve |
|---|---|---|

## Budget
Current 180 + proposed 14 = 194 lines. Under the 200-line target. No cuts required.
```

Then tell the user, in one line, how to verify the accepted file actually loaded: start a fresh
session and run **`/memory`**, which lists every instruction file loaded into the current session. If
their file is not in that list, the agent has never seen it and every rule in it is decoration.

## What this skill will not do

- **It will not write your `CLAUDE.md`.** It writes `CLAUDE.md.candidates.md` and stops. Every rule in
  it is a claim about how the user wants to work, mined from their worst-tempered moments — a human
  reads the list and picks. An agent that silently rewrites the file the agent itself reads is a loop
  nobody asked for.
- **It will not invent rules.** Every candidate cites the session, the timestamp, and the sentence the
  user actually typed. A rule with no correction behind it is the "write clean code" line that made
  their old file worthless.
- **It will not install the hooks it recommends.** It identifies which corrections are really
  enforcement and hands them over with the `exit 2` warning attached. Wiring a `PreToolUse` hook that
  can block your own commits is a change with blast radius, and a human signs that off.
- **It will not audit the skills you already have.** That is `vc-skill-auditor` — dead skills,
  colliding triggers, descriptions the model never matches on.
