harness-memory
Health Gecti
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Community trust — 22 GitHub stars
Code Uyari
- process.env — Environment variable access in .github/scripts/ensure-embedding-model.mjs
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Project memory layer for coding harnesses
harness-memory
Trigger-aware project memory for Claude Code. harness-memory learns policies and pitfalls from your own sessions — a correction you typed, a command that failed and was then fixed — and surfaces them before the tool runs, at the PreToolUse boundary, instead of dumping a handbook into the system prompt.
It is complementary to claude-mem: claude-mem remembers what happened; harness-memory stops the agent at the moment it is about to repeat a known mistake. No account, no telemetry, no daemon, no LLM call per tool call — the hot path reads one precomputed JSON file.
0.6.0-rc.1 — Claude Code only. Codex hooks arrive in 0.6.1 and OpenCode (V2 plugin) in 0.6.2. v0.5 was an OpenCode plugin; read Upgrading from 0.5 before installing 0.6 over an existing
.harness-memory/. Not on npm yet: install the pre-release tarball, see Install.
- How it works in one turn
- Requirements
- Install
- How memory is captured and saved
- The authority model
- What gets injected, when
- Embeddings (optional)
- Configuration
- CLI reference
- Troubleshooting
- Privacy and security
- Upgrading from 0.5
- Roadmap
- Development
How it works in one turn
you: don't run `npm test` here, run `pnpm test` from packages/api
└─ UserPromptSubmit hook: correction detected (the previous tool call failed) → ledger line
claude: (fixes it, runs pnpm test) … Stop
└─ Stop hook: closes the turn as an episode (evidence weight 2), writes queue/<id>.json,
spawns the detached worker
worker: your own `claude -p` (headless, no tools) extracts one fact:
pitfall · signature bash/cmd "npm test" · scope packages/api → validated →
active (authority auto, enforcement advise, TTL 30 d) → snapshot rebuilt
next session:
claude: Bash: npm test
└─ PreToolUse hook (≈ 50 ms, no SQLite): signature matches — `npm test`, `npm run test` and
`& npm.cmd test` canonicalize the same way; `pnpm test` never matches it → additionalContext:
"[harness-memory] PITFALL (auto, provisional): Run pnpm test from packages/api …"
An auto rule is born at advise. When a second session confirms the correction it climbs to
soft_deny, and the same call is denied: "… If this does not apply, call the tool again to proceed."
claude: Bash: pnpm test
Three ideas carry the design:
- Timing. Knowledge has four lifecycle triggers —
session_start,before_model,before_tool,after_tool— and the one that matters most isbefore_tool: Claude Code's only pre-execution channel ispermissionDecision: ask | deny, so every memory carries an enforcement level (advise,confirm,soft_deny,deny) that maps onto it. - Evidence is not memory. Hooks only append raw events to a per-session ledger. Memories are structured facts (type, scope glob, trigger signature, triggers, confidence, provenance) produced by one write path: episode → extraction → validation → deterministic reconcile.
- Governed autonomy. The LLM extracts; authority stays explicit. A pitfall learned from one correction activates on its own (
authority = auto, with a TTL and a lower trust multiplier). Human memories can never be demoted, superseded or rejected by the automatic path — it may only propose, and you decide inhm review.
Requirements
- Node.js ≥ 20 (developed on 24). Hooks are plain
nodeprocesses; the worker runs SQLite through sql.js (WASM) — no native dependencies. - Claude Code ≥ 2.1.139 for exec-form hooks (
{"command": "<node>", "args": [...]}). Older versions get shell-form hooks automatically whenclaude --versioncan be detected; with noclaudebinary found the version is unknown and exec form is assumed — if Claude Code then reports a hook error on every call, re-runhm install --host claude --hook-form shell. - An extraction CLI, logged in from a terminal, for learning:
claude(preferred) orcodex. Extraction runs through your own headless CLI and subscription — no API key. The login is optional for enforcement: the hooks fire every rule already in the snapshot (hm memory:add, imported rows) without it; turning newhm importbullets into typed rows and learning from your sessions — including what you ask it to remember — start afterclaude auth login(older CLIs:claude, then/login; the desktop app's login does not apply toclaude -p). Until then bullets and episodes wait in the queue and nothing is lost. - Windows, macOS, Linux. On Windows, Git Bash is optional (see below).
Install
Install the release candidate
0.6.0-rc.1 is not on npm yet: npm install -g harness-memory still installs 0.5.1, the OpenCode plugin, which has no hm binary. Install the tarball attached to the GitHub pre-release instead:
node --version # ≥ 20; with nvm, the version you will keep (nvm alias default <v>)
npm uninstall -g harness-memory # removes a leftover 0.5.1; harmless when nothing is installed
npm install -g https://github.com/smc2315/harness-memory/releases/download/v0.6.0-rc.1/harness-memory-0.6.0-rc.1.tgz
hm --version # 0.6.0-rc.1
cd <your project>
hm install --host claude # from a plain terminal, not from a shell inside a Claude Code session
hm status
hmnot found. The npm global prefix (npm prefix -g; Windows default%APPDATA%\npm) is not on PATH, or the terminal predates the Node install — open a new one. Meanwhilenode "$(npm root -g)/harness-memory/dist/bin/harness-memory.js" --versionworks.harness-memory --versionprinting0.5.1means another Node's global directory wins:npm ls -g harness-memoryunder each Node.- Windows PowerShell:
hm.ps1 cannot be loaded because running scripts is disabledis npm's PowerShell shim under the default execution policy (npm's ownnpm.ps1fails the same way). Once:Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, or runhm.cmd/ use Git Bash. The hooks never use the shim: they runnode.exedirectly. - Per project instead of global:
npm install -D <same URL>, thennpx hm install --host claude; the hooks then point atnode_modules/harness-memory/dist/hook.js. Notnpx harness-memory …while npm'slatestis 0.5.1 (that runs the old plugin'sinstall), and not from git (github:smc2315/harness-memory#redesign/v0.6has nodist/; the build runs only inprepack). - Later:
npm install -g harness-memory@nextonce the rc is published under thenexttag, plainharness-memoryonce 0.6.0 islatest. After any update runhm install --host claudeagain in each project: it migrates the database (backup first), replaces the hook entries, re-probes and re-runs the self-test. A global install keeps its hook path, so nothing breaks in between. - Learning needs the
claudeCLI logged in from a terminal:claude auth login(older CLIs:claude, then/login). The desktop app's login does not count, and the desktop app alone puts noclaudeon your PATH — install the CLI; without ithm installreportsprovider none foundand still succeeds. Enforcement of imported and hand-added rules works without the login;probe not_logged_in/queue N (waiting: not_logged_in)are expected until then, andhm install --host claudere-probes at once afterwards. - Moving to another machine: nothing to copy.
.harness-memory/is per project and git-ignored, so a fresh clone starts empty; to carry memories, copy that project'smemory.sqlite(andconfig.json) with Claude Code closed and runhm install --host claudethere. Never copy.claude/settings.local.jsonorinstall-manifest.json: they hold the other machine's node and hook paths. Korean step-by-step checklist: docs/setup-new-machine.ko.md.
Abridged output of hm install --host claude on a new project:
harness-memory 0.6.0-rc.1 installed for Claude Code
project C:\src\my-app
hooks 9 events → C:\src\my-app\.claude\settings.local.json (exec form)
node C:\Program Files\nodejs\node.exe (claude 2.1.172)
hook script C:\Users\me\AppData\Roaming\npm\node_modules\harness-memory\dist\hook.js
database C:\src\my-app\.harness-memory\memory.sqlite v22 (created)
snapshot built (active 0, candidates 0)
config C:\src\my-app\.harness-memory\config.json (created); embeddings off
git .claude/settings.local.json added to C:\src\my-app\.git\info\exclude
git C:\src\my-app\.harness-memory\.gitignore written (nothing under .harness-memory/ is ever committed)
provider claude — probe not_logged_in (Not logged in · Please run /login); the hooks enforce rules without the login — …; learning from sessions … starts after `claude auth login` …
self-test session-start ok (105ms, hook status ok)
manifest C:\src\my-app\.harness-memory\install-manifest.json
Open Claude Code in this project; `hm status` shows what the hooks learn.
A project that already has a v0.5 database shows v22 (migrated from v9, backup …\memory.sqlite.bak-v9) instead; read Upgrading from 0.5 first.
No restart is needed: Claude Code's file watcher picks up the new hook entries (if hm doctor → hooks.calls still says never after your first prompt, start a new session). Open the project in Claude Code, work normally, and watch hm status. If the project has a CLAUDE.md or AGENTS.md, the install output also suggests hm import --from CLAUDE.md, which turns its bullets into human memories after a confirmation table.
Claude Code's own auto memory (~/.claude/projects/<slug>/memory/, notes with type: feedback) is a second import source: hm import --from auto-memory turns those notes into human rules through the same confirmation table, so a correction Claude already wrote down starts being enforced at the tool boundary. The install output mentions it when such notes exist.
What hm install writes
| Path | What |
|---|---|
.claude/settings.local.json |
Nine hook entries (table below). Existing hooks are untouched; re-running hm install replaces only entries whose args point at a harness-memory hook.js. --user targets ~/.claude/settings.json instead (CLAUDE_CONFIG_DIR is honoured). |
.git/info/exclude |
Gains the line .claude/settings.local.json (project scope only). When .git is a worktree or submodule pointer file, the real info/exclude is resolved through it; when it cannot be resolved, install warns instead of staying silent. |
.harness-memory/.gitignore |
Contains *, so the ledgers, the database, the snapshot and the logs never reach git. An edited file is kept; hm doctor warns when git already tracks files under the directory and prints the git command that untracks them. |
.harness-memory/ |
Created with config.json defaults, memory.sqlite (created, or migrated with a memory.sqlite.bak-v<N> backup when upgrading), the snapshot/ files and empty ledger/ state/ queue/ raw/ locks/ logs/. |
.harness-memory/install-manifest.json |
Files touched with sha256, node path, hook form, version. hm uninstall and hm doctor --fix work from it. |
~/.harness-memory/ |
User-level, created on first use rather than by install: worker/<repo_hash>/ (an empty cwd for the headless extraction CLI, outside any git repository), models/ (embedding model cache). A v0.5 global.sqlite there is left alone. |
Each registered hook is an exec-form command — the absolute Node binary that ran hm install plus the absolute dist/hook.js and the event name:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell|Edit|MultiEdit|Write|NotebookEdit|WebFetch|mcp__.*",
"hooks": [
{
"type": "command",
"command": "C:\\Program Files\\nodejs\\node.exe",
"args": ["C:\\...\\harness-memory\\dist\\hook.js", "pre-tool"],
"timeout": 5
}
]
}
]
}
}
| Claude Code event | matcher | hook argv | timeout |
|---|---|---|---|
SessionStart |
— | session-start |
10 s |
UserPromptSubmit |
— | prompt |
10 s |
PreToolUse |
Bash|PowerShell|Edit|MultiEdit|Write|NotebookEdit|WebFetch|mcp__.* |
pre-tool |
5 s |
PostToolUse |
.* |
post-tool |
5 s |
PostToolUseFailure |
.* |
post-tool-failure |
5 s |
Stop |
— | stop |
10 s |
StopFailure |
— | stop-failure |
10 s |
PreCompact |
manual|auto |
precompact |
10 s |
SessionEnd |
— | session-end |
1 s |
Read, Grep, Glob and Task are deliberately not registered on PreToolUse (no 50 ms process on the most frequent tools); a memory whose signature names one of them is re-labelled to before_model by the validator.
Node path, npx, and Windows shells
- Node path. Hooks record the exact
nodebinary that ranhm install(process.execPath, resolved through symlinks and Windows junctions, so fnm, nvm-windows and Scoop links pin the real version) and the exactdist/hook.jsof that package. That makes them immune to PATH, nvm and volta switches — but if that Node version goes away or the package moves, the hooks stop silently:brew upgrade noderemoves the oldCellar/node/<version>(install warns),nvm uninstalldoes the same, and a reinstall under pnpm / yarn / npx changes the path.hm doctorreports it (install.node,install.hook_script) andhm doctor --fixrewrites the entries to the current binary and script; with nvm,nvm alias default <v>beforehm installkeeps the pinned version around. The CLI itself needsnodeon PATH (hmis a#!/usr/bin/env nodescript); the hooks do not. - npx, pnpm, yarn. Do not run
npx harness-memory …while npm'slatestis 0.5.1: it fetches the old OpenCode plugin and runs itsinstall. pnpm and npx (and yarn when the package lands under.yarn/) keep one directory per version, so the registereddist/hook.jsdies with the next upgrade (install warns; runhm install --host claudeagain afterwards). npm installs — global or projectdevDependency— keep the same path across upgrades. - Git Bash / PowerShell (Windows). Claude Code runs
Bashthrough Git Bash and exposes aPowerShelltool when Git Bash is absent. harness-memory maps both to the canonical toolbashand records the dialect, so a pitfall learned under one shell fires under the other (pnpm test; git statusand& npm testcanonicalize the same way). Exec-form hooks involve no shell at all. If shell form is needed (old Claude Code), install picks"shell": "bash"when Git Bash is found (CLAUDE_CODE_GIT_BASH_PATH,Program Files\Git,%LOCALAPPDATA%\Programs\Git,PATH) and"powershell"otherwise. The detached worker runs withwindowsHide, so no console window flashes. Shell-form entries for PowerShell carry the call operator (& "<node>" "<hook.js>" <event>); without it PowerShell prints the quoted command instead of running it. - WSL. Install the package and run
hm installinside the environment that runs Claude Code: a WSL session needs the Linuxnodeand a Linuxdist/hook.jspath in.claude/settings.local.json, and a checkout under/mnt/cholds one settings file, so pick one side. With a user-space Node (nvm, a tarball under$HOME) source its environment in the shell that runshm, and keep the prefix out of/tmp, which systemd clears at every WSL boot.
Verify and remove
hm status # one line: active N (auto M) | candidates | proposals | queue | last extraction | probe | snapshot | p95
hm doctor # every check with OK / WARN / FAIL; --self-test runs the session-start hook once
hm uninstall # removes the hook entries, the git exclude line and the manifest; keeps .harness-memory/
hm uninstall --purge # also deletes .harness-memory/ except memory.sqlite.bak-v<N> backups
How memory is captured and saved
hooks (no DB) ledger/*.jsonl ──► queue/<episode>.json ──► worker extract ──► memory.sqlite ──► snapshot/*.v1.*
state/*.json (turn segment) claude -p + (one lock, read by every hook
V1–V12 + reconcile write_ops)
1. Ledger — what the hooks write
Every hook appends one JSON line (≤ 32 KB, a single appendFileSync, credentials redacted first) to .harness-memory/ledger/claude-code-<session_key>.jsonl, where session_key = sha1("claude-code:" + session_id).slice(0, 16). Hooks never open SQLite; the only other hook-written files are state/claude-code-<session_key>.json (open turn: turn_key, turn_start_offset, segment, correction_for) and queue/*.json.
| line | written by | fields (caps) |
|---|---|---|
prompt |
prompt |
text (4,000 chars), correction, remember |
fired |
pre-tool |
memory_ids, rule_codes, tool, target_digest (120), delivery, decision, enforcement, signature_keys |
tool_ok |
post-tool |
tool (canonical), host_tool, shell_dialect, input_digest (500), scope_refs, program_token, output_head (2,000) / output_tail (1,000), resolution_of |
tool_fail |
post-tool-failure |
the tool_ok fields + error (1,500), exit_code, is_interrupt, failure_class (exit_N, enoent, eacces, timeout, interrupt, other), failure_sig |
assistant |
stop |
text = last assistant message (2,000) |
marker |
session-start, precompact, session-end, stop-failure, idle sweep |
marker, detail |
Regexes are used only to set flags — they never produce memory text (the v0.5 "template candidate" defect). Signals:
- correction — the previous turn's last tool call failed, or the prompt matches correction vocabulary (
don't,instead,wrong,revert,아니,하지 마,말고,대신,틀렸, …). - remember —
remember,from now on,기억,앞으로,절대;always/항상only at a sentence start or together with another cue. The flag weights the episode (3). The human path of §7.3 additionally needs an explicit request in the prompt itself (remember this,기억해,from now onfollowed by a rule);I don't remember whether …never counts. - failure → resolution — a
tool_okfor the same tool and the same path (or the same canonical program token, e.g.pm test) as an earlier unresolvedtool_fail. A failingnpm testfollowed by a successfullsis not a resolution; a successfulpnpm testis. - decision marker —
decided,we'll use,switch to,결정,이제부터,전환, … in the last assistant message.
2. Episode — the turn segment is the queue
Stop, PreCompact and SessionEnd close the open segment ([turn_start_offset, EOF), ≤ 4 MB) into queue/<episode_id>.json: scope refs, tools used, failure signatures with their resolutions, flags, which memories fired and whether the agent overrode or heeded a soft_deny, and
evidence_weight = correction 2 + remember 3 + (resolution 2 | unresolved failure 1 per failure) (cap 10)
Only episodes with evidence_weight ≥ 1 or a decision marker go to the LLM; read-only turns are skipped as no_signal. The next UserPromptSubmit closes a turn the host never finished (interrupted); the SessionStart hook and hm dream sweep turns idle for more than an hour (idle). Episode ids are deterministic (host:session:turn_key:segment), so re-closing the same segment is a no-op and a turn compacted mid-way yields two episodes.
3. Extraction — a detached worker and your own CLI
The hook spawns node dist/worker.js <root> extract detached (stdio: ignore, windowsHide), so the hook returns in milliseconds. The worker, under locks/worker.lock:
inserts the queued episodes (
INSERT OR IGNORE), rollsfire_count/override_countup from the fired lists, leases ≤ 3 episodes — corrections first — and applies the daily cap (extraction.max_runs_per_day, default 40; correction episodes are exempt);renders the prompt: episode header, the previous turn's last ≤ 12 tool lines as non-citable context when this is a correction, every ledger line numbered
e<seq>(failures and resolutions in full, the rest summarized), the last assistant message, up to 40 existing memories whose scope intersects, the canonical tool vocabulary and the type boundary rules — capped at 24,000 chars;calls the provider (
extraction.provider:auto→claudeif found, elsecodex):claude -p --output-format json --json-schema <schema> --tools "" --disallowedTools "mcp__*" \ --strict-mcp-config --no-session-persistence \ --settings '{"disableAllHooks":true,"autoMemoryEnabled":false}' --model <extraction.model, default sonnet>with the prompt on stdin, cwd
~/.harness-memory/worker/<repo_hash>/(outside every repository, so noCLAUDE.md, project hooks or auto-memory load), an allowlisted environment (noCLAUDECODE,CLAUDE_CODE_*;HARNESS_MEMORY_DISABLE=1so hooks inside the child exit immediately), a 120 s wall clock and the raw answer cached 24 h inraw/<run_id>.json. The binary is resolved fromHARNESS_MEMORY_CLAUDE_BIN, thenCLAUDE_CODE_EXECPATH, thenPATH, then the winget package directory. A probe (a one-field schema call) classifies the provider asok,not_logged_in,missing,nested_refusal,probe_timeoutorinvalid_output; anokis trusted for a day, a failure is retried after ten minutes, and unavailable providers leave episodesqueuedwithout burning an attempt.
The extractor answers a fixed JSON schema: episode_summary, nothing_to_learn and ≤ 8 facts, each with action (create | reinforce | propose_supersede | propose_stale), type, summary (12–160 chars), details, scope_glob, relevant_tools, trigger_signature ({tool, target, pattern_kind, pattern}), lifecycle_triggers, confidence, evidence_refs (e<seq> citations), knowledge_kind, rationale and aliases (≤ 8 synonyms, including the user's language). There is no delete and no edit: the LLM can only create, reinforce or propose.
4. Validation — V1 to V12
Each fact passes a deterministic validator before it touches the database: schema shape (V1); every evidence_refs entry resolves to a line of this episode (V2); the scope is a repository-relative glob that intersects the episode's files unless a signature carries the precision (V3); the signature's tool was actually used in the episode (V4); regexes compile, have no nested quantifiers and sweep 4 KB samples within the budget — 2 ms, or 2.5× a reference pattern of the same shape measured alongside on a slower machine (V5); the security scanner passes and the text does not talk about harness-memory itself (V6); reinforce / propose_* targets exist (V7); the fact is not a restatement of the prompt (V8); no duplicate semantic_key within the batch (V9); signatures are specific — cmd ≥ 2 tokens, substring ≥ 8 chars, globs with a path fragment (V10); before_tool is kept only where a hook can deliver it (V11); and a signature is rejected when it also matches the command that resolved the cited failure while that command differs from the failing one — pm test would fire on the pnpm test that fixed npm test (V12). A run that is not JSON, has more than 8 facts or more than 50 % hallucinated citations is marked suspect and skipped whole; otherwise rejected facts are counted and the rest are applied.
5. Reconcile — one transaction, then the snapshot
Under locks/db.lock (held for seconds, never around the LLM call) the worker applies the facts fact-by-fact (SAVEPOINT each): create becomes a new row with authority = 'auto', status = 'candidate', source_ref = 'episode:<id>'; an identical or near-identical live row (semantic_key, or Jaccard ≥ 0.6 on the normalized summary) is reinforced instead; propose_* opens or supports a proposal. Every fact records memory_provenance (episode, cited line, confidence); derived counters (evidence_count, evidence_score = Σ episode weights, distinct_session_count, auto confidence) are recomputed immediately, then decideTier decides active or candidate (see auto tier). The run id is written to write_ops, so replaying the same run is a no-op; policy_rules are re-derived from active policy memories; db_generation increments; the database is saved atomically (.tmp-<pid> → fsync → .prev → rename) and snapshot/*.v1.* is rebuilt. A corrupted main file is recovered from .prev.
Consolidation (worker dream; spawned at SessionEnd when the last run is older than dream.interval_hours = 6, or hm dream) never invents memories: TTL expiry (correction/failure-derived rows get a revalidate proposal first), auto candidates older than 45 d → rejected, proposals past 30 d → expired, TTL extension for rows that fired recently, override downgrade (an auto row overridden ≥ 3 times with evidence_score < 3 drops to advise), merge proposals for auto near-duplicates, embedding backfill, the idle-turn sweep and garbage collection (ledger segments 14 d, raw/ 24 h, tmp files 10 min, audit log 30 d).
.harness-memory/ layout — what to back up
| Path | Content | Back up? |
|---|---|---|
memory.sqlite |
Source of truth: memories, episodes, runs, proposals, provenance, transitions, policy rules, write_ops, meta |
Yes |
memory.sqlite.prev, memory.sqlite.bak-v<N>, memory.sqlite.corrupt-<ts>, memory.sqlite.rollback-<ts> |
Last good copy; pre-migration backup per schema version; damaged file kept aside; file replaced by db:rollback |
keep .bak-v<N> until you trust the upgrade |
config.json |
Your settings (see Configuration) | Yes |
install-manifest.json |
What hm install touched |
no (re-run hm install) |
snapshot/active.v1.json, lexical.v1.json, recall.v1.json, dense.v1.bin, .reindex-requested |
Read model for the hooks: active memories with precompiled scope regexes and signatures, derived rules, failure index, counts; serialized MiniSearch index; per-episode recall summaries with their own index (when recall.enabled); optional embedding matrix |
no (derived; hm snapshot:rebuild) |
ledger/*.jsonl, state/*.json, state/*.notify.json |
Per-session raw events (14 d), open-turn cursor, worker → next-prompt notices | no |
queue/*.json, raw/*.json |
Episodes waiting for extraction; provider raw output cache (24 h; invalid or suspect answers are kept as <run>.<attempt>.json for the same time) |
no |
locks/db.lock/, locks/worker.lock/, *.pid, embed-helper.spawn |
mkdir-based locks with owner.json heartbeats; pid files; spawn throttle |
no |
logs/hooks.log, logs/worker.log, logs/audit.log |
One line per hook call (5 MB × 3 rotation); worker events; audit events | no |
hm install writes .harness-memory/.gitignore (*), so nothing in the directory reaches git; hm doctor warns if files there were committed earlier. hm memory:export writes a deterministic SQL dump for backups that should live in version control.
The authority model
Two axes on every memory
authority ∈ {human, auto}— who may change its status.humanrows come fromhm memory:add,hm import(a file you wrote),hm memory:promote, acceptedactivate_*proposals, replacement rows you accept, and any human edit (memory:enforce, scope changes). They have no TTL, a trust multiplier of 1.0, and the automatic path can never demote, supersede, stale or reject them —transitionMemorythrowsAuthorityViolationErrorand writes nothing.autorows are born from extraction; they carry a TTL, a trust multiplier of 0.65 → 0.8 → 0.95 as validations accumulate, and they can be demoted by 2-session contradicting evidence or by repeated overrides.enforcement ∈ {advise, confirm, soft_deny, deny}— how abefore_toolhit is delivered.denycan only be set by a human; the LLM never raises enforcement.
The delivery ladder (Claude Code)
| enforcement | what Claude Code receives | timing |
|---|---|---|
advise |
hookSpecificOutput.additionalContext |
with the tool result |
confirm |
permissionDecision: "ask" + reason; downgraded to soft_deny under permission_mode bypassPermissions / dontAsk (delivery = before_downgraded) |
before execution |
soft_deny |
permissionDecision: "deny" + reason ending in "If this does not apply, call the tool again to proceed." — only the first attempt of a (memory, tool) pair in a turn is denied; the retry passes and the override is recorded |
before execution |
deny |
permissionDecision: "deny" every time ("This action is blocked by a human-set policy.") |
before execution |
confirm, soft_deny and deny also emit one systemMessage line for you (harness-memory: pitfall 3f9a… fired (soft_deny)); advise is silent. The injected text looks like [harness-memory] PITFALL (auto, provisional): <summary> — <details>; labels are human, human, unconfirmed import or auto, provisional, plus , contested on a human advise row that two sessions have argued against. Reasons are ≤ 600 chars; the hook never emits allow.
Default enforcement (enforcementDefault, always overridable with hm memory:enforce)
| memory | authority | default |
|---|---|---|
hard policy with a signature; a confirmed import rule with never / must not / always / do not / 절대 / 반드시 / 항상 and a signature |
human | confirm (before_tool.hard_policy) |
soft policy, policy without a signature, hm import --yes rows (import_unconfirmed = 1), other imports |
human | advise |
auto pitfall with a specific signature and knowledge_kind ∈ {correction, failure_resolution} |
auto | advise (before_tool.auto_enforcement); climbs to soft_deny when a second session reinforces it (before_tool.auto_escalation, default { enabled: true, sessions: 2 }; autonomy_level: shadow never climbs; a row downgraded by overrides climbs again only after a new session reinforces it). Shown in hm memory:history as active (enforcement) by auto_rule |
a rule from a turn with an explicit remember request (remember this, 기억해, from now on + a rule) whose fact cites that prompt line |
human (status_reason remember, active at once, at most 2 per turn) |
policy with a signature → confirm (before_tool.hard_policy); pitfall with a signature → soft_deny; everything else → advise. Named at SessionStart for 24 h with its undo (hm memory:demote <id>); the automatic path never touches it afterwards |
every other auto row (pitfall without signature, workflow, decision, architecture_constraint) |
auto | advise |
Auto tier — when a memory activates on its own
decideTier runs for every created or reinforced auto candidate, right after its counters are recomputed:
- active when all hold:
type ∈ {pitfall, workflow}, a valid repository-relative scope, a trigger signature orrelevant_tools, a trigger a hook can reach,confidence ≥ 0.7,evidence_score ≥ 2, the scanner is clean, the row is not a replacement row of an open proposal, the caps are not hit (extraction.max_auto_activations_per_episode= 3,max_auto_activations_per_day= 10) andautonomy_level = standard. The row getsttl_expires_at = now + 30 dfor correction / failure-resolution knowledge (7 d for conventions),revalidate_afterat half that,validation_count = evidence_count − 1. - candidate otherwise — with a reason (
evidence,cap,confidence,type,replacement,autonomy, …) and, forpolicy, anactivate_policyproposal; for a signature-less global row, anactivate_globalproposal.decisionandarchitecture_constraintalways wait for you.
"Enough evidence" is a weight sum, not an episode count: one correction (2) or one failure → resolution pair (2) activates immediately. The cost side is the TTL, the 0.65 trust multiplier at first activation, the 2-session rule for demotion, V10 specificity, the override downgrade and the daily cap. Reinforcement never shrinks a TTL (ttl = max(existing, now + max(7·2^validations, base)), capped at 90 d); a fired memory is extended by 14 d.
Proposals and review
Eight proposal kinds: supersede, stale, activate_policy, activate_global, fix_scope (from the v0.5 migration), fix_signature, revalidate, merge. One open proposal per (kind, target, candidate); repeated claims only raise support_count / support_sessions. The extractor's propose_stale / propose_supersede against a human row only opens a proposal and marks the row contested; against an auto active row it is applied automatically only with support from ≥ 2 sessions, at least one of which carries a human signal (a correction or remember) or a deterministic observation — an agent cannot erase your correction in two of its own turns. Rejecting a proposal suppresses the same claim for 30 days; proposals expire after 30 days (fix_scope: 90).
Review is exception handling, with a budget (≤ 3 items per active day is the dogfood gate): hm review walks proposals on human memories → revalidate → policy candidates → global candidates → fix_scope / fix_signature → merge → unconfirmed imports → human candidates, with keys accept, reject, skip, trace, promote. Accepting a supersede makes the replacement row human; accepting activate_* promotes the row as hm memory:promote would. The SessionStart hook shows a one-line systemMessage digest once a day (harness-memory: N proposal(s) open, M expiring within 7 days — run hm review). No review text ever reaches the model's context.
autonomy_level
standard (default) — the table above. shadow — decisions are only logged to audit_log (auto_activation_shadow); nothing activates. off — decideTier never activates anything.
What gets injected, when
| trigger | Claude Code hook | what | budget |
|---|---|---|---|
session_start |
SessionStart |
active rows with session_start in their triggers and type baseline / decision / architecture_constraint / workflow; plus the daily digest systemMessage |
5 items / 3 KB |
before_model |
UserPromptSubmit (once per user turn) |
baseline rows always; other rows when their scope intersects path tokens in the prompt (or the last touched file) and BM25 over summary / details / aliases ≥ before_model.min_score — or, with the embedding helper, cosine ≥ 0.80; then, when recall.enabled is on, up to before_model.recall.max_items summaries of past episodes from other sessions ([harness-memory] RECALL <date>: …, BM25 over snapshot/recall.v1.json ≥ before_model.recall.min_score, plain context, never enforced) |
5 items / 3 KB (+ 2 items / 1 KB recall) |
before_tool |
PreToolUse |
the ladder above | 3 items / 2 KB |
after_tool |
PostToolUseFailure |
failure_index[failure_class] hints (e.g. memories about enoent on this tool) as additionalContext |
2 items / 1 KB |
before_tool in detail: the hook canonicalizes the tool (Bash / PowerShell → bash, Edit / MultiEdit / NotebookEdit → edit, WebFetch → fetch, mcp__server__tool → mcp:server/tool), picks the target (command, file_path, url or the MCP input JSON) and the path-like tokens in it, reads the ledger tail to learn what already fired this turn, then filters the snapshot: status and TTL → trigger → relevant_tools → signature match (cmd patterns match on the canonical command form: npm test ≡ npm run test ≡ & npm.cmd test, while the family forms pm test / bin vitest cover every package manager / runner; glob / regex / substring on the target) or, for signature-less policy / pitfall rows, scope regex × path tokens → derived policy rules → ranking by enforcement, signature match, human first, score → quotas (policy 2, pitfall 2, workflow 1) → budget. Rows imported from a file the host already loads (CLAUDE.md, CLAUDE.local.md) are skipped at session_start / before_model so nothing is said twice. hm memory:why --tool bash --target "npm test" runs exactly this function and prints every activation and suppression reason.
Embeddings (optional)
Dense retrieval is off by default and never on the PreToolUse path. When you want it for before_model:
hm install --host claude --with-embeddings # npm install @xenova/transformers in the package root, embeddings.enabled = true
--with-embeddings runs npm install @xenova/transformers inside the installed package directory (for a global install $(npm root -g)/harness-memory), which also pulls that manifest's devDependencies: about 350 MB and 17 s on a fast machine, measured on the rc tarball. It needs write access there — a sudo-installed prefix refuses (installed with problems; the hooks are registered all the same); then install the dependency next to the package instead, npm install -g @xenova/transformers (with sudo if that prefix needs it; Node resolves it from the parent node_modules), and set embeddings.enabled: true in config.json. Never run hm install itself with sudo: it would leave root-owned files in the project. The next package upgrade drops the in-package copy, so run hm install --host claude --with-embeddings again afterwards. A leaner, upgrade-proof location under ~/.harness-memory/ is planned for 0.6.1.
- A small helper process (
dist/embed-helper.js <root>) serves top-k queries over a Windows named pipe (\\.\pipe\harness-memory-embed-<repo_hash>) or a Unix socketharness-memory-embed-<repo_hash>.sockin$XDG_RUNTIME_DIR(else the temp directory) — never under the repository, so a deep checkout stays below the Unix socket path limit;hm doctorprints the path. Theprompthook asks it with abefore_model.dense_timeout_ms(80 ms) budget and falls back to lexical-only when it does not answer; the first miss spawns the helper detached (throttled to once per 60 s). It loadssnapshot/dense.v1.bin, reloads when that file changes, and exits by itself afterembeddings.idle_exit_minutes(15) of inactivity or on SIGTERM. There is no installed service and no fixed port. - Model:
Xenova/multilingual-e5-small(quantized), cached under~/.harness-memory/models(embeddings.cache_dir). The download happens once, only when embeddings are enabled and the model is absent; it is the only network access besides your own extraction CLI. - The worker embeds new rows right after extraction (
passage:prefix) andhm dreambackfills the rest;dense.v1.binis only written when at least one row has an embedding. - Korean and other aliases need no embeddings: the extractor emits up to eight
aliasesper fact (in the user's language when the prompt was not English), and the lexical snapshot indexes them with a code-aware, Hangul-particle-stripping tokenizer, so a Korean prompt reaches an English memory through BM25.
Configuration
.harness-memory/config.json (schema config.v1). Unknown keys are ignored and invalid values fall back to the defaults, so a bad config never breaks a hook.
| key | default | meaning |
|---|---|---|
autonomy_level |
standard |
off / shadow / standard (see above) |
before_tool.max_items / max_bytes |
3 / 2048 | injection budget at PreToolUse |
before_tool.auto_enforcement |
advise |
where correction / failure-derived auto pitfalls with a signature are born (advise or soft_deny) |
before_tool.auto_escalation.enabled / sessions |
true / 2 | the autonomy ladder: an auto advise pitfall becomes soft_deny once its evidence spans this many sessions |
before_tool.hard_policy |
confirm |
default for hard human policies with a signature (confirm, advise, deny) |
before_model.max_items / max_bytes |
5 / 3072 | injection budget at UserPromptSubmit |
before_model.min_score |
0.35 | normalized BM25 threshold (s / (s + 1)) for non-baseline rows |
before_model.dense |
helper |
helper (use the embedding helper when embeddings.enabled) or off |
before_model.dense_timeout_ms |
80 | how long the prompt hook waits for the helper |
before_model.recall.max_items / max_bytes / min_score |
2 / 1024 / 0.5 | recall items appended after the memory items at UserPromptSubmit |
recall.enabled / keep_episodes |
false / 300 | the recall layer (episode summaries, snapshot/recall.v1.json, hm recall); off by default until it proves useful |
session_start.max_items / max_bytes |
5 / 3072 | startup budget |
after_tool.max_items / max_bytes |
2 / 1024 | failure-hint budget |
extraction.provider |
auto |
auto, claude, codex, replay (HARNESS_MEMORY_PROVIDER overrides) |
extraction.model |
null |
--model for the headless CLI. null makes the claude provider pass the sonnet alias (your interactive default in ~/.claude/settings.json may be a gateway id the headless first-party call cannot reach) and lets codex use its own default |
extraction.max_runs_per_day |
40 | LLM calls per day; correction episodes are exempt |
extraction.max_auto_activations_per_day / _per_episode |
10 / 3 | auto activation caps |
extraction.timeout_ms / probe_timeout_ms / lease_seconds |
120000 / 20000 / 180 | provider wall clock, probe wall clock, episode lease |
embeddings.enabled / model / idle_exit_minutes / cache_dir |
false / Xenova/multilingual-e5-small / 15 / null (= ~/.harness-memory/models) |
embedding helper |
dream.interval_hours |
6 | consolidation cadence (checked at SessionEnd) |
retention.ledger_days / raw_hours / audit_days / candidate_days / proposal_days |
14 / 24 / 30 / 45 / 30 | garbage collection and expiry |
log.debug / max_bytes / rotations |
false / 5 MiB / 3 | hooks.log verbosity and rotation |
Environment variables: HARNESS_MEMORY_DISABLE=1 makes every hook exit before reading anything; HARNESS_MEMORY_DEBUG=1 adds a full JSON detail line per hook call to logs/hooks.log; HARNESS_MEMORY_HOME relocates ~/.harness-memory; HARNESS_MEMORY_CLAUDE_BIN / HARNESS_MEMORY_CODEX_BIN pin the extraction binaries; HARNESS_MEMORY_PROVIDER forces a provider.
CLI reference
Binary harness-memory, alias hm. Every command accepts --json (machine-readable output), --db <path> (database file; <x>/.harness-memory/memory.sqlite also selects project <x>) and --root <dir> (project root; default: the nearest ancestor holding .harness-memory/, else .git/, else the cwd). hm <command> --help lists a command's options; hm --version prints the version.
Exit codes: 0 ok · 1 error · 2 usage error, unknown command, or a command not in this build · 75 the database lock is busy (another writer holds locks/db.lock; retry).
hm eval:live(the live three-arm evaluation) is the only commandhm --helpstill marks(not in this build); it prints a one-line explanation and exits 2.
| Command | What it does |
|---|---|
| Install | |
hm install --host claude [--user] [--with-embeddings] [--hook-form auto|exec|shell] [--hook-script <path>] [--skip-probe] [--skip-self-test] |
Register the hooks, migrate the database (backup first), build the snapshot, probe the extraction provider and run the hook self-test. --user writes ~/.claude/settings.json; --with-embeddings installs @xenova/transformers and enables dense retrieval. |
hm uninstall [--purge] [--user] |
Remove the hook entries the manifest points at, the .git/info/exclude line and the manifest; --purge also deletes .harness-memory/ except memory.sqlite.bak-v<N>; --user (without a manifest) also strips ~/.claude/settings.json. |
hm doctor [--fix] [--host-debug <path>] [--self-test] [--hook-script <path>] |
Cross-check manifest vs registered hooks, node path, last call per event and p95 from hooks.log, database (quick_check, user_version), snapshot freshness, unreachable memories, queue depth, last extraction, probe, locks, embeddings, Git Bash. --fix rewrites moved node / hook paths and rebuilds the snapshot; --self-test runs the session-start hook once; --host-debug counts finished with status N lines in a Claude Code debug log. |
hm db:migrate |
Apply pending schema migrations under the lock (copies memory.sqlite to .bak-v<N> first) and rebuild the snapshot. |
hm db:rollback [--to <version>] [--yes] |
Restore memory.sqlite.bak-v<N> (newest by default) after a y/N confirmation (--yes / -y when there is no TTY); keeps the replaced file as memory.sqlite.rollback-<ts> and removes .prev and the snapshot. The next writer migrates forward again — downgrade or hm uninstall first. |
hm snapshot:rebuild |
Rebuild snapshot/active.v1.json, lexical.v1.json, recall.v1.json when recall.enabled (and dense.v1.bin) from the database; migrates first when needed. |
| Status | |
hm status |
One line: active N (auto M) | candidates C | proposals P (E expiring) | queue Q (waiting: reason) | last extraction <ago> <status> | probe <result> | snapshot fresh|stale|missing | p95 <ms>, plus a probe: hint line when the provider needs attention. Exit 1 when not installed. |
| Review | |
hm review [--json] [--list] [--keys <a,r,s,p,q>] [--accept-all-kind <kind>] |
Interactive review of proposals, candidates and unconfirmed imports in the order described above (keys a / r / s / t / p / q). Non-interactive paths: --list prints the queue without prompting, --keys replays a comma-separated key sequence over the queue (scripts, tests), --accept-all-kind <kind> accepts every open proposal of one kind (or import to confirm all unconfirmed imports), --json prints the queue as JSON. |
hm proposals:list [--kind <kind>] |
List open proposals. |
hm proposals:accept <id> | --kind <kind> --all |
Accept one proposal, or every open proposal of a kind. |
hm proposals:reject <id> |
Reject a proposal (the same claim is skipped for 30 days). |
| Memory | |
hm memory:list [--status <s>] [--type <t>] [--authority <a>] [--source <k>] [--scope <glob>] [--unconfirmed] [--contested] [--limit <n>] |
List memories — live ones (candidate, active, stale) by default, --status all for every status — filtered by type, authority, source kind, exact scope, unconfirmed imports or contested rows (200 rows max by default). |
hm memory:show <id> |
Show one memory with its signature, scope, authority, enforcement, counters and TTL. |
hm memory:add --type <t> --summary <text> [--scope <glob>] [--details <text>] [--triggers <a,b>] [--signature <spec>] [--tools <a,b>] [--enforce <level>] [--aliases <a,b>] [--policy-subtype <s>] [--confidence <n>] [--importance <n>] [--active] |
Add a human memory (authority = human, source_kind = manual) — a human candidate for hm review, or active right away with --active. --signature is tool:target:kind:pattern (e.g. bash:command:cmd:npm test; prefix ! to negate); triggers, tools and enforcement default from the type and signature. |
hm memory:promote <id> |
Promote a candidate (or stale row) to active as a human — the row becomes human and loses its TTL. |
hm memory:demote <id> |
Demote an active memory. |
hm memory:reject <id> [--note <text>] |
Reject a memory, optionally keeping a note. |
hm memory:restore <id> |
Restore a rejected / stale / superseded memory to active. |
hm memory:enforce <id> <advise|confirm|soft_deny|deny> |
Set the enforcement level (a human edit: the row becomes human). |
hm memory:history <id> |
Status transitions, provenance and — for one release — the v0.5 dream history from the legacy_* tables. |
hm memory:export [--out <file>] |
Deterministic SQL dump of the database (same content → identical bytes). |
hm memory:export-skill [--out <dir>] [--min-confidence <n>] [--min-importance <n>] |
Export mature memories (confidence and importance at or above the thresholds) as SKILL.md files, by default under .harness-memory/skills. |
hm memory:stats [--since <iso>] |
Counts by status / authority / type, review load per day (gate: ≤ 3), recent auto activations and override distribution over the window (default: the last 7 days). |
hm memory:why --tool <name> --target <text> [--input <json>] [--session <key>] [--permission-mode <m>] |
Explain what would fire for a tool call: activated items, suppressed ones with the reason (signature_mismatch, already_fired, scope_mismatch, …) and matching policy rules. --tool takes the host name (Bash, Edit, mcp__server__tool) or the canonical one; --input passes raw tool_input JSON instead of --target; --session derives "already fired this turn" from that session's ledger; --permission-mode simulates bypassPermissions / dontAsk. |
hm memory:trace <id> |
Episodes and extraction runs behind a memory. |
| Pipeline | |
hm queue [list|show <id>|retry <id>|drop <id>] |
Inspect or edit the extraction queue (queued, leased, failed, dead episodes). |
hm extract [--now] [--provider <name>] [--dry-run] |
Run the extraction worker in the foreground; --dry-run renders the prompts without calling the provider. |
hm dream |
Run consolidation now (TTL, expiry, override downgrade, merge proposals, embedding backfill, GC). |
hm trace <session_key> |
Ledger timeline of a session: prompts, fired memories, tool calls, failures and resolutions. |
hm recall <query> [--limit <n>] [--since <iso>] |
Search past episodes by their summaries — what before_model recalls when recall.enabled is on: prints date, session (<host>-<key>, usable with hm trace), kind (extracted = the extractor's summary, derived = the worker's deterministic one) and summary, newest-relevant first. Reads snapshot/recall.v1.json when it is fresh, else the database (and says which). --since filters on closed_at; the command applies no score threshold. With recall disabled it says so and exits 0. |
| Import | |
hm import [--from <file>] [--from auto-memory] [--auto-memory] [--include-project] [--memory-dir <dir>] [--yes] [--sync] [--dry-run] [--provider <name>] |
Split CLAUDE.md / AGENTS.md / .cursorrules bullets — or, with --from auto-memory (--auto-memory), the type: feedback notes of Claude Code's auto memory under ~/.claude/projects/<slug>/memory/ (--include-project adds type: project notes; --memory-dir <dir> names the directory when it cannot be derived) — into import episodes, run the extractor with action = create, show a confirmation table and insert the confirmed rows as human / import memories with source_ref = import:<file>#L<n> (import:auto-memory:<file>#L<n>). Bullets that cannot be typed yet (no extraction CLI) are stored verbatim as advise rows and typed later. --yes skips confirmation (import_unconfirmed = 1, enforcement capped at advise, shown in hm review); --sync stales rows whose bullets disappeared (directory-wide for auto-memory); --dry-run previews. Re-runs are idempotent by line hash. |
| Evaluation | |
hm eval:latency [--local] [--runs <n>] [--threshold <ms>] [--memories <n>] [--large] [--hook-script <path>] |
Spawn dist/hook.js pre-tool 50× against a generated 300-memory fixture and report min / p50 / p95 / max; exits 1 when p95 exceeds the budget (default 90 ms, --threshold). --local measures the installed project instead of a fixture, --large adds an informational 2,000-memory pass, --hook-script points at another dist/hook.js. |
hm eval:replay [--scenario <ids>] [--fixtures <dir>] [--keep] [--verbose] |
Replay the recorded ledger fixtures s01–s12 through the real hooks and the worker with the replay provider and check the scenario assertions (needs the repository checkout's test/fixtures/replay, or --fixtures); exits 1 on any failing check. --keep leaves each scenario's temp project on disk. |
hm eval:live |
Live three-arm evaluation (markdown only / native auto-memory / harness-memory) — later release. |
Names and flags are taken from the dispatcher (src/cli/registry.ts and the command modules under src/cli/commands/); test/docs-cli-reference.test.ts fails when this table and the dispatcher disagree.
Troubleshooting
Read hm status first. active 12 (auto 4) | candidates 3 | proposals 1 | queue 2 (waiting: not_logged_in) | last extraction 3h ago ok | probe not_logged_in | snapshot fresh | p95 61ms tells you how many rows can fire, what is waiting for review, whether episodes are stuck and why, whether the provider works, whether the hooks read a current snapshot, and how slow PreToolUse is. Then hm doctor.
| Symptom | What it means / what to do |
|---|---|
probe not_logged_in, queue N (waiting: not_logged_in) |
The headless claude -p is not authenticated (the desktop app's login does not carry over). Run claude auth login in a terminal (older CLIs: claude, then /login); the worker re-probes within ten minutes and drains the queue, or run hm install --host claude to re-probe at once. Nothing is lost meanwhile. |
probe missing |
No claude / codex binary found. Install one, or set HARNESS_MEMORY_CLAUDE_BIN / HARNESS_MEMORY_CODEX_BIN. |
probe nested_refusal |
The probe ran inside a Claude Code session (CLAUDECODE inherited). Run hm install / hm extract from a plain terminal. |
probe provider_error |
The headless CLI ran but its API call failed; hm status and hm doctor print the provider's message next to the result (for example api 404: There's an issue with the selected model …). Set extraction.model in .harness-memory/config.json to a model your account can use (the default is the sonnet alias) and run hm install --host claude to re-probe. |
probe invalid_output / probe_timeout |
claude -p --output-format json --json-schema did not return structured output within extraction.probe_timeout_ms. Check claude -p in a terminal; a failed result is retried after ten minutes. |
Nothing fires, hooks.calls says never for every event |
Claude Code is not calling the hooks: check .claude/settings.local.json (hm doctor compares it to the manifest), that you opened Claude Code in this project, and the node path (install.node). hm doctor --self-test runs the hook exactly as Claude Code would. |
install.node FAIL / install.hook_script FAIL |
The recorded node binary or dist/hook.js moved (nvm switch, package reinstall, npx cache pruned). hm doctor --fix. |
snapshot stale for more than a minute |
Hooks saw a database newer than the snapshot and requested a reindex (snapshot/.reindex-requested, throttled to one per 60 s) but node dist/worker.js <root> reindex did not run — look at logs/worker.log, or run hm snapshot:rebuild. |
queue grows, dead episodes in hm doctor |
Three failed attempts (1 h / 6 h back-off) make an episode dead; hm queue show <id> has last_error, hm queue retry <id> requeues it. |
exit code 75 |
Another writer holds locks/db.lock (worker or CLI). Retry; a stale lock (heartbeat > 120 s or dead pid) is taken over automatically and reported by hm doctor (locks.db). |
database … is newer than this build |
The database was migrated by a newer harness-memory; upgrade the package. Hooks and read-only commands keep working. |
| Memories fire too often / wrong memory | hm memory:why --tool bash --target "<command>" explains every suppression; hm memory:enforce <id> advise lowers a row; an auto row overridden three times downgrades itself. |
A confirm policy never asks |
Under permission_mode bypassPermissions / dontAsk there is nobody to ask, so confirm becomes soft_deny (delivery = before_downgraded in the fired line). |
Windows: Claude Code shows a PowerShell tool |
Git Bash is absent; memories still match (canonical bash). hm doctor reports env.git_bash. |
hm: command not found / 'hm' is not recognized after npm install -g |
The npm global prefix is not on PATH (npm prefix -g; Windows default %APPDATA%\npm, nvm ~/.nvm/versions/node/<v>/bin, Homebrew /opt/homebrew/bin) or the terminal predates the install — open a new one. Meanwhile node "$(npm root -g)/harness-memory/dist/bin/harness-memory.js" works. harness-memory --version printing 0.5.1 means another Node's global directory wins (npm ls -g harness-memory under each Node); hm did not exist in 0.5. |
Windows PowerShell: hm.ps1 cannot be loaded because running scripts is disabled |
npm's PowerShell shim under the default execution policy (npm.ps1 fails the same way). Once: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, or run hm.cmd / use Git Bash. The hooks never use the shim. |
| Claude Code shows a hook error on every tool call right after install | Exec-form hooks need Claude Code ≥ 2.1.139 (inferred from release notes); install assumes exec form when claude --version cannot be detected. Re-run hm install --host claude --hook-form shell (Windows without Git Bash: PowerShell, with the & call operator). |
probe not_logged_in although claude works in your terminal |
The worker runs claude -p with an allowlisted environment. The CLI's own auth and routing variables are forwarded (CLAUDE_CONFIG_DIR, CLAUDE_CODE_OAUTH_TOKEN, CLAUDE_CODE_USE_BEDROCK / USE_VERTEX, AWS_*, GOOGLE_*, NODE_EXTRA_CA_CERTS, SSL_CERT_FILE / SSL_CERT_DIR, REQUESTS_CA_BUNDLE); anything else your login depends on is not. hm doctor --json shows the probe detail. |
logs/hooks.log has one line per hook call:
2026-11-08T12:00:01.234Z pre-tool tool=bash gen=42 activated=1 suppressed=17 decision=deny delivery=before enforcement=soft_deny dur=48ms ok
status is ok, bad_stdin, snapshot_missing, disabled, not_installed, skipped:<reason> or error:<code>; gen is the snapshot's db_generation. HARNESS_MEMORY_DEBUG=1 (or log.debug: true) adds a JSON line with the full detail (activated ids, suppression kinds, rule hits). Hooks never fail the host: any exception becomes one log line, {} on stdout and exit 0. logs/worker.log has the worker's lease / extraction / end lines (lease … waiting=<reason> names what blocks the queue). HARNESS_MEMORY_DISABLE=1 in Claude Code's environment turns every hook into an immediate exit when you need to rule harness-memory out.
Privacy and security
- No account, no telemetry, no server. Everything lives in
<project>/.harness-memory/and~/.harness-memory/. The only processes that talk to a network are (1) the extraction CLI you already use —claude -porcodex execunder your own login, called by the detached worker — and (2) the optional, one-time embedding model download when you enable--with-embeddings. - What the extractor sees. For an eligible episode: your prompt text (≤ 4,000 chars), the tool calls of the turn as digests (command text or repository-relative path, ≤ 500 chars), tool output head/tail (2,000 / 1,000 chars) and error text (1,500) for failing and resolving calls, the last assistant message (≤ 2,000), the previous turn's ≤ 12 tool lines when the turn is a correction, and up to 40 existing memories (id, type, status, authority, scope, signature, summary). Never the transcript file, never your file contents beyond what tool output already showed. The call runs with
--tools "", all hooks and auto-memory disabled, no session persistence, an allowlisted environment and a cwd outside every repository. - Redaction before anything is written. Ledger lines pass
redactCredentialsfirst: private key blocks,sk-ant-…,sk-…, GitHubghp_…/github_pat_…, GitLabglpat-…, npmnpm_…, Stripesk_live_…/sk_test_…, AWSAKIA…, Slackxox…/xapp-…, GoogleAIza…, JWTs,Bearer …andBasic …headers, credentials in a URL of any scheme (postgres://user:pass@),curl -u user:pass,--password …,mysql -p…, andKEY=value/"key": "value"/key: valueassignments whose key names a password, secret, token or API key (DB_PASSWORD=,AWS_SECRET_ACCESS_KEY=,accessToken:) become<redacted>;$VARreferences are kept. The same redaction runs overscope_refs,failure_sigandprogram_token(capped at 256 chars per token, 4,000 in total) and over the text of imported bullets. - Extracted text is scanned (invisible Unicode and control characters, prompt-injection phrases in English and Korean such as "ignore previous instructions" / "이전 지시를 무시", credential assignments, instructions to conceal or to disobey the user) and facts that mention harness-memory, its commands or memory ids are rejected (V6), so the model cannot write itself a rule through a tool output. The extraction prompt wraps recorded events, the previous turn, the last assistant message and the existing-memory list in data fences and states that fenced content is data, never an instruction; a legacy candidate whose stored text fails the scan can be reinforced but never auto-activates.
- Hooks are read-only. They open no database, hold no lock and return exit 0 on every path; their only writes are the ledger, the session state and queue files.
PreToolUsenever emitsallow, so it can never widen your own permission prompts. - Retention. Ledger segments 14 d, raw provider output 24 h, audit log 30 d, hooks.log 5 MB × 3 — configurable under
retention/log.hm uninstall --purgeremoves it all except thememory.sqlite.bak-v<N>backups.
Upgrading from 0.5
v0.5 was an OpenCode plugin with a different write path (conversation buffer → evidence rows → dream consolidation → human-reviewed promotion). v0.6 keeps your memory.sqlite and the five types / four triggers, and rebuilds everything around it:
hm install --host claudemigratesuser_version9 / 10 / 16 → 22 (migrations 017–022), after copying the file tomemory.sqlite.bak-v<N>. No memory row is deleted. Every row gets anauthority:candidaterows and auto-promoted rows becomeauto, every other rowhuman; v0.5 LLM rows with scope**/*and onlybefore_modelgetneeds_scope = 1and afix_scopeproposal (90 d) inhm review; the v0.5 dream tables are renamedlegacy_*(read byhm memory:historyfor one release), template candidates are rejected, duplicate live rows getmergeproposals.- OpenCode is not supported by 0.6.0 (V2 plugin in 0.6.2; Codex hooks in 0.6.1). Remove the v0.5 plugin entry from
opencode.json(and.opencode/package.json,.opencode/commands/harness-memory-*.md) yourself;hm uninstall --purgeremoves the legacy.conversation-buffer.json/.dream-extract-state.jsonfiles. hm db:rollbackrestores the pre-migration backup; the next writer migrates forward again, sohm uninstallfirst when you mean to go back to 0.5.
Details, the migration table and the old → new command map are in docs/redesign/upgrade-from-0.5.md.
Roadmap
- 0.6.0 — Claude Code (this release). Gates:
eval:replaygreen,eval:latencyp95 ≤ 90 ms, two weeks of dogfood with ≤ 3 review items per active day and an autonomy regret (rejected auto activations) ≤ 10 %. - 0.6.1 — Codex host (
.codex/hooks.json,apply_patch,turn_id, thecodex execprovider). - 0.6.2 — OpenCode V2 plugin (
Plugin.define,permission.assert, context hook). - 0.7 — global memory sync, auto↔auto merge, heeded-fire inference, snapshot buckets, dropping the
legacy_*tables.
Development
npm install
npm run build # tsup → dist/hook.js, hook-bundle.js, worker.js, embed-helper.js, bin/harness-memory.js (+ migrations)
npm test # vitest
npm run typecheck # tsc --noEmit
npm run package:verify # pack, install the tarball into a temp project, run hm install / status / doctor
| Directory | Purpose |
|---|---|
src/core/ |
Shared contracts: vocabularies, ledger / episode / extraction / snapshot / hook types, config, paths, ids, errors, process helpers |
src/adapters/ |
Hook core (runHook), hook I/O, the Claude Code host adapter, session state |
src/activation/ |
Snapshot reader, before_tool / before_model / session_start / after_tool selection, signature canonicalization and matching, tool vocabulary, lexical snapshot, the ActivationEngine |
src/dream/ |
Ledger, episodes, extraction prompt / schema / validation, providers (claude, codex, replay), reconcile, consolidate, worker |
src/promotion/ |
transitionMemory and the §4.7 edge table, decideTier / enforcementDefault, proposals, revalidation |
src/policy/ |
Policy engine (tool + argument signature matching) and rule derivation |
src/db/ |
Store (lock → load → migrate → transaction → atomic save → snapshot), locks, migrator, migrations 001–022, repositories, snapshot builder |
src/memory/ |
MemoryRepository |
src/embed/ |
Embedding helper, client and the dense.v1.bin codec |
src/cli/ |
The hm dispatcher, shared libs and command modules |
src/security/ |
Content scanner |
Design documents: docs/redesign/spec-v0.6.md (Korean, the authoritative design), docs/redesign/implementation-plan.md, docs/redesign/m0-facts.md (facts measured on a real machine), docs/redesign/phase-a-notes.md and docs/redesign/phase-b-notes.md (module APIs). Pages under docs/product/ and docs/spec/ describe v0.5 and carry a banner saying so.
Korean README: README.ko.md. Changes: CHANGELOG.md.
License
MIT
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi