Persistent memory layer for Claude Code compaction — improves post-compact context quality
npm install compahookPersistent memory layer for Claude Code's /compact command. Improves post-compact context quality by extracting, scoring, and re-injecting high-signal information that would otherwise be lost during conversation compaction.
When Claude Code compacts a conversation, the built-in summary can lose important details: architectural decisions, error resolutions, file edit purposes, and explicitly marked context. After compaction, the model may re-ask questions or forget critical constraints.
Three hooks that work in sequence around the compact cycle:
```
[Normal work] ─── PostToolUse ──→ logs edits/commands to working-state.jsonl
│
[/compact triggers] ─ PreCompact ──→ reads transcript, scores items,
writes compact-context.json,
outputs compact instructions to stdout
│
[Session resumes] ── SessionStart ─→ reads compact-context.json,
injects structured markdown via
additionalContext
The result: after compaction, the model receives a structured summary of decisions, modified files, unresolved issues, and explicitly marked context — ranked by relevance.
- Node.js >= 18
- Claude Code CLI installed
`bash`
npm install -g compahook
compahook install
This installs three commands (compahook-collect, compahook-extract, compahook-restore) into your PATH and registers the hooks in ~/.claude/settings.json.
`powershell`
npm install -g compahook
compahook install
Same commands — npm creates .cmd shims on Windows that are resolved by cmd.exe when Claude Code spawns hooks (it uses shell: true internally). No additional configuration needed.
Existing users can update to the latest version:
`bash`
npm install -g compahook@latest
`bash`
compahook status
Expected output:
``
compahook: installed (PostToolUse, PreCompact, SessionStart)
| Command | Description |
|---------|-------------|
| compahook install | Register hooks in ~/.claude/settings.json |compahook uninstall
| | Remove hooks from settings |compahook status
| | Check if hooks are installed |compahook stats
| | Display performance metrics for current project |compahook stats --global
| | Aggregate metrics across all registered projects |compahook watch [ms]
| | Live-refreshing metrics dashboard |compahook watch --global [ms]
| | Live global metrics across all projects |compahook reset-metrics
| | Clear recorded metrics for current project |
Every time Claude Code uses Write, Edit, MultiEdit, or Bash, the collector appends a one-line JSON entry to :
`jsonl`
{"ts":1706000000,"type":"file_edit","file":"src/auth.js","tool":"Write"}
{"ts":1706000001,"type":"command","cmd":"npm test"}
Automatically prunes to 500 lines (keeps newest 400).
When /compact triggers, the extractor:
1. Reads the conversation transcript (JSONL provided by Claude Code)
2. Classifies each message: goals, decisions, file edits, commands, errors, markers
3. Loads previous context and increments cycle ages for carried items
4. Deduplicates items using type-aware keys with NFKC normalization
5. Scores items using: recency(position^2) typeWeight markerBoost * decay(cycleAge)
6. Filters stale items below threshold, enforces hard cap (500 items)
7. Takes the top 30 scored items for output
8. Merges with recent working-state entries
9. Writes
10. Outputs natural-language compact instructions to stdout (tells the compactor what to preserve)
When the session resumes after compaction, the restorer:
1. Reads compact-context.json (only if < 5 minutes old — prevents stale injection)additionalContext
2. Formats a structured markdown summary (max 4000 chars)
3. Outputs it as which Claude Code injects into the new session
The injected context looks like:
`markdownsrc/server.jsSession Memory (restored after compaction)
$3
Build REST API with JWT authentication$3
- Using Express with jsonwebtoken (score: 0.95)
- Dual-token strategy: 15min access, 7d refresh (score: 0.90)$3
- (Write)src/auth/middleware.js
- (Edit)`$3
- bcrypt native module failed, switched to bcryptjs$3
- Token expiry 15min access, 7d refresh
- Rate limiting 5 attempts per minute
Items are ranked by a composite score:
| Factor | Formula |
|--------|---------|
| Recency | (position / total)^2 — recent items score higher |exp(-cycleAge / halfLife)
| Type weight | decision: 1.0, error: 1.5, goal: 0.85, file_edit: 0.7, command: 0.5, read: 0.3 |
| Marker boost | 1.5x multiplier for messages containing IMPORTANT:, REMEMBER:, NOTE:, CRITICAL: |
| Cycle decay | — items decay ~63% per 10 compaction cycles |
The extractor now includes additional processing:
- Deduplication: Type-aware deduplication with NFKC normalization prevents duplicate entries
- Hard cap: Maximum 500 preserved items with score-based truncation
- Cycle age tracking: Items carry their age across compaction cycles for decay calculation
- Threshold filtering: Stale carried items below minScoreThreshold are pruned (fresh items always pass)
Create to override defaults:
`json`
{
"maxContextSize": 4000,
"maxItems": 30,
"maxWorkingStateLines": 500,
"pruneKeepLines": 400,
"recencyExponent": 2,
"markerKeywords": ["IMPORTANT:", "REMEMBER:", "NOTE:", "CRITICAL:", "TODO:", "FIXME:"],
"markerBoost": 1.5,
"stalenessMinutes": 5,
"decayHalfLife": 10,
"minScoreThreshold": 0.001,
"maxPreservedItems": 500,
"enableTelemetry": true,
"typeWeights": {
"decision": 1.0,
"error": 1.5,
"goal": 0.85,
"file_edit": 0.7,
"command": 0.5,
"marker": 1.0,
"read": 0.3,
"generic": 0.2
}
}
All fields are optional — unspecified values use defaults.
Per-project memory is stored in :
| File | Purpose |
|------|---------|
| working-state.jsonl | Rolling log of file edits and commands |compact-context.json
| | Structured context from last compaction |config.json
| | Optional per-project configuration |metrics.json
| | Performance metrics and token savings tracking |telemetry.jsonl
| | Pipeline telemetry logs (when enableTelemetry: true) |
Global state is stored in ~/.claude/:
| File | Purpose |
|------|---------|
| compahook-projects.json | Registry of active projects (max 200, self-pruning) |
These files are created automatically. Add .claude/memory/ to your .gitignore.
`bash`
npm test
Runs 48 unit tests covering scorer, parser, collector, extractor, and restorer.
compahook tracks its own performance and token savings in real time. Every hook invocation records latency, classification stats, and injection size to .
`bash`
compahook stats
Sample output:
`
compahook metrics (session: 1h 23m)
──────────────────────────────────────────
Compact cycles: 3
Total items tracked: 187
Items preserved: 90 (top 30 per cycle)
Noise filtered: 97 items dropped below threshold
Token budget:
Injected: 1,068 tokens (across 3 cycles)
Est. waste prevented: ~7,500 tokens
Net saving: ~6,432 tokens
Hook latency (avg):
Collector: 1.2ms
Extractor: 5.8ms
Restorer: 0.3ms
Collector activity:
File edits logged: 42
Commands logged: 15
Total invocations: 57
Score distribution (cumulative):
file_edit: 48 items
command: 31 items
decision: 12 items
marker: 8 items
goal: 6 items
error: 3 items
Top scores (last cycle): 1.2279, 0.7347, 0.7, 0.6667, 0.4535
Score range: 0 - 1.2279 (avg: 0.2731)
`
`bash`
compahook watch # refreshes every 2s
compahook watch 5000 # custom interval (ms)
Displays the same metrics dashboard with live refresh. Press Ctrl+C to stop.
`bash`
compahook reset-metrics
Clears all recorded metrics for the current project. Useful at the start of a benchmarking session.
Monitor compahook performance across all projects on the machine:
`bash`
compahook stats --global
Sample output:
`
compahook global metrics (4 projects)
──────────────────────────────────────────────────
Compact cycles: 12
Total items tracked: 487
Items preserved: 360
Noise filtered: 127
Token budget (all projects):
Injected: 4,320 tokens
Est. waste prevented: ~30,000 tokens
Net saving: ~25,680 tokens
Hook latency (avg across all):
Collector: 1.1ms
Extractor: 4.9ms
Restorer: 0.4ms
Per-project breakdown:
/home/user/project-alpha
cycles: 5 saved: ~10,240 tokens edits: 38
/home/user/project-beta
cycles: 4 saved: ~8,720 tokens edits: 25
/home/user/project-gamma
cycles: 3 saved: ~6,720 tokens edits: 19
`
Live global monitoring:
`bash`
compahook watch --global # refresh every 2s
compahook watch --global 5000 # custom interval
How it works: Each time a hook fires, the project path is registered in ~/.claude/compahook-projects.json. The registry is self-managing:
- Capped at 200 entries — evicts least-recently-seen when full
- Auto-prunes on read — removes entries whose metrics.json no longer exists
- Staleness eviction — drops entries not seen in 30+ days
The file stays under 20KB permanently regardless of how many projects you work on.
| Metric | Source | Description |
|--------|--------|-------------|
| Compact cycles | Extractor | Number of times /compact has run |
| Items classified | Extractor | Total transcript messages parsed and scored |
| Items preserved | Extractor | Items that made the top-N cut |
| Noise filtered | Extractor | Items dropped below threshold |
| Tokens injected | Restorer | Characters injected / 4 (approximate) |
| Waste prevented | Extractor | Conservative estimate: ~2500 tokens saved per cycle |
| Hook latency | All hooks | Execution time per hook invocation |
| Type distribution | Extractor | Breakdown by decision, error, goal, file_edit, etc. |
| Score distribution | Extractor | Min/max/avg scores and top 10 from last cycle |
| Collector activity | Collector | File edits vs commands logged |
Metrics recording never blocks hook execution — all recording is wrapped in try/catch with silent failure.
`bash`
npm run benchmark
Measures extractor quality against a fixed transcript with 15 ground-truth facts:
- Coverage: % of ground-truth facts captured
- Precision: % of extracted items that are substantive
- Size efficiency: facts per 1000 characters of output
- Latency: extractor execution time
`bash`
npm uninstall -g compahook
This automatically removes hooks from ~/.claude/settings.json (via the preuninstall lifecycle script) without affecting other hook configurations.
To remove hooks without uninstalling the package:
`bash`
compahook uninstall
compahook is hardened against common filesystem and input attacks:
| Control | Implementation |
|---------|---------------|
| Path traversal | validateCwd() rejects null bytes, non-absolute paths, over-length paths |isSymlink()
| Symlink attacks | check before all file writes |0o600
| Stdin DoS | 1MB size cap on all stdin handlers |
| File permissions | for files, 0o700 for directories |open → fstat → read → close
| Atomic writes | Temp file + rename for metrics, settings, and context |
| TOCTOU races | pattern in restorer |
| Prototype pollution | Schema validation with whitelisted keys in config |
| Processing caps | 5000 item limit, 1MB line limit, 10MB file size cap |
| Transcript validation | Type, size, path, and file-type checks before parsing |
| Command matching | Exact Set-based lookup for hook command names |
Debug logging is available via COMPAHOOK_DEBUG=1 (outputs to stderr to avoid corrupting hook stdout).
| Concern | Status |
|---------|--------|
| Settings.json location | os.homedir() + '/.claude/settings.json' — works on all platforms |shell: true
| Hook command resolution | Claude Code uses — resolves npm .cmd shims on Windows natively |path.join()
| Path separators | All paths use — correct separators per OS |crlfDelay: Infinity
| Line endings | Parser uses ; all splits use .trim() to handle \r |.cmd
| Shebangs | Irrelevant on Windows — npm shims call node` directly |
compahook uses only Node.js built-in modules (fs, path, os, readline). No external dependencies.
MIT