compahook
v1.1.2
Published
Persistent memory layer for Claude Code compaction — improves post-compact context quality
Downloads
952
Maintainers
Readme
compahook
Persistent 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.
Problem
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.
Solution
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
additionalContextThe result: after compaction, the model receives a structured summary of decisions, modified files, unresolved issues, and explicitly marked context — ranked by relevance.
Requirements
- Node.js >= 18
- Claude Code CLI installed
Installation
Linux / macOS
npm install -g compahook
compahook installThis installs three commands (compahook-collect, compahook-extract, compahook-restore) into your PATH and registers the hooks in ~/.claude/settings.json.
Windows
npm install -g compahook
compahook installSame 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.
Updating
Existing users can update to the latest version:
npm install -g compahook@latestVerify installation
compahook statusExpected output:
compahook: installed (PostToolUse, PreCompact, SessionStart)CLI commands
| 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 |
How It Works
1. Collector (PostToolUse)
Every time Claude Code uses Write, Edit, MultiEdit, or Bash, the collector appends a one-line JSON entry to <project>/.claude/memory/working-state.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).
2. Extractor (PreCompact)
When /compact triggers, the extractor:
- Reads the conversation transcript (JSONL provided by Claude Code)
- Classifies each message: goals, decisions, file edits, commands, errors, markers
- Loads previous context and increments cycle ages for carried items
- Deduplicates items using type-aware keys with NFKC normalization
- Scores items using:
recency(position^2) * typeWeight * markerBoost * decay(cycleAge) - Filters stale items below threshold, enforces hard cap (500 items)
- Takes the top 30 scored items for output
- Merges with recent working-state entries
- Writes
<project>/.claude/memory/compact-context.json - Outputs natural-language compact instructions to stdout (tells the compactor what to preserve)
3. Restorer (SessionStart)
When the session resumes after compaction, the restorer:
- Reads
compact-context.json(only if < 5 minutes old — prevents stale injection) - Formats a structured markdown summary (max 4000 chars)
- Outputs it as
additionalContextwhich Claude Code injects into the new session
The injected context looks like:
## Session Memory (restored after compaction)
### Current Task
Build REST API with JWT authentication
### Key Decisions
- Using Express with jsonwebtoken (score: 0.95)
- Dual-token strategy: 15min access, 7d refresh (score: 0.90)
### Files Modified This Session
- `src/server.js` (Write)
- `src/auth/middleware.js` (Edit)
### Unresolved Issues
- bcrypt native module failed, switched to bcryptjs
### Important Context
- Token expiry 15min access, 7d refresh
- Rate limiting 5 attempts per minuteScoring System
Items are ranked by a composite score:
| Factor | Formula |
|--------|---------|
| Recency | (position / total)^2 — recent items score higher |
| 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 | exp(-cycleAge / halfLife) — items decay ~63% per 10 compaction cycles |
v2 Scoring Pipeline (v1.1.0+)
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
minScoreThresholdare pruned (fresh items always pass)
Configuration
Create <project>/.claude/memory/config.json to override defaults:
{
"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.
Storage
Per-project memory is stored in <project>/.claude/memory/:
| 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.
Running Tests
npm testRuns 48 unit tests covering scorer, parser, collector, extractor, and restorer.
Monitoring & Metrics
compahook tracks its own performance and token savings in real time. Every hook invocation records latency, classification stats, and injection size to <project>/.claude/memory/metrics.json.
View current project metrics
compahook statsSample 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)Live monitoring
compahook watch # refreshes every 2s
compahook watch 5000 # custom interval (ms)Displays the same metrics dashboard with live refresh. Press Ctrl+C to stop.
Reset metrics
compahook reset-metricsClears all recorded metrics for the current project. Useful at the start of a benchmarking session.
Global monitoring (cross-project)
Monitor compahook performance across all projects on the machine:
compahook stats --globalSample 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: 19Live global monitoring:
compahook watch --global # refresh every 2s
compahook watch --global 5000 # custom intervalHow 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.
What's tracked
| 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.
Benchmarking
npm run benchmarkMeasures 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
Uninstalling
npm uninstall -g compahookThis automatically removes hooks from ~/.claude/settings.json (via the preuninstall lifecycle script) without affecting other hook configurations.
To remove hooks without uninstalling the package:
compahook uninstallSecurity
compahook is hardened against common filesystem and input attacks:
| Control | Implementation |
|---------|---------------|
| Path traversal | validateCwd() rejects null bytes, non-absolute paths, over-length paths |
| Symlink attacks | isSymlink() check before all file writes |
| Stdin DoS | 1MB size cap on all stdin handlers |
| File permissions | 0o600 for files, 0o700 for directories |
| Atomic writes | Temp file + rename for metrics, settings, and context |
| TOCTOU races | open → fstat → read → close 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).
Platform Notes
| Concern | Status |
|---------|--------|
| Settings.json location | os.homedir() + '/.claude/settings.json' — works on all platforms |
| Hook command resolution | Claude Code uses shell: true — resolves npm .cmd shims on Windows natively |
| Path separators | All paths use path.join() — correct separators per OS |
| Line endings | Parser uses crlfDelay: Infinity; all splits use .trim() to handle \r |
| Shebangs | Irrelevant on Windows — npm .cmd shims call node directly |
Zero Dependencies
compahook uses only Node.js built-in modules (fs, path, os, readline). No external dependencies.
License
MIT
