Version: 1.0.0 Last updated: 2026-05-01 Status: Active Issue: #534 Epic: #505
- Purpose and Scope
- Assets
- Adversary and Failure Models
- Defense Layers Overview
- Threat R1 — Sync Silent Failure
- Threat R2 — Contaminated Memory Propagation
- Threat R3 — SSH Signing Key Loss
- Threat R4 — Audit Findings Overload
- Threat R5 — Backfill Destroys Data
- Threat R6 — Validator False-Positives
- Threat R7 — GitHub Outage / Repo Loss
- Residual Risks
- Versioning
This document is the canonical security analysis of the cross-machine memory sync system implemented in epic #505. It enumerates the seven threat categories surfaced during epic design, maps each to the existing defense layer responsible for catching it, and calls out the residual risk that no automated control fully closes.
- The git-backed memory store at
~/.claude/memory-shared/and its remotekcenon/claude-memory. - The sync engine
memory-sync.shand its pre-push and post-pull validation stages. - The PreToolUse
memory-write-guard.sh, SessionStartmemory-integrity-check.sh, and PostToolUsememory-access-logger.shhooks. - The validators
validate.sh,secret-check.sh,injection-check.sh(in theclaude-memoryrepository). - The weekly
audit.shand optional monthlysemantic-review.sh. - Branch protection and CI gating on
kcenon/claude-memory.
- Threats to the host operating system, local filesystem, or local SSH agent.
- Network-layer threats below TLS (the user's ISP, the Claude API endpoint).
- Threats targeting GitHub itself (account takeover at the platform level, GitHub-side data loss).
- Threats targeting the Claude Code binary, Claude API, or Anthropic infrastructure.
- Multi-tenant or cross-user threats — this is a single-user system; the user is trusted and is not modeled as an attacker against themselves.
MEMORY_SYNC.md— operations runbook (companion document)MEMORY_VALIDATION_SPEC.md— validator contract, exit codes, schema rulesMEMORY_TRUST_MODEL.md— trust tier semanticsSSH_COMMIT_SIGNING.md— per-machine signing setup
| Asset | Sensitivity | Notes |
|---|---|---|
Memory file content (memories/*.md body) |
Medium | Project context, decisions, feedback rules. Not credentials by design (secret-check.sh enforces). |
| Memory frontmatter | Medium | Author, source machine, trust level. Provenance — tampering enables forgery. |
MEMORY.md index |
Low | Generated artifact derived from frontmatter. Drift surfaces as warning, not authority. |
| User identity (signing key, GitHub handle, email in commits) | Low | Already public via existing repository commits. |
~/.claude/logs/memory-access.log |
Low | Path-only access record; never transmitted; rotates monthly. Local to each machine. |
| Session-specific transient memory | High | Never written to disk. Out of scope. |
SSH signing key (per machine, in ~/.ssh/) |
High | Provenance authority. Compromise covered under R3. |
Branch protection on kcenon/claude-memory main |
Critical | Server-side gate. Compromise covered under R7. |
This system models three failure modes — automated, accidental, and adversarial:
| Mode | Example | Primary mitigation surface |
|---|---|---|
| Automated failure | launchd dies, network drop, validator false-positive | SessionStart integrity check, idempotent retry, warn-only validators |
| Accidental misuse | User mis-edits a memory, runs backfill on wrong tree | Backups, dry-run defaults, git history |
| Adversarial input | Prompt-injection-driven self-reinforcing memory text | 5-layer validation chain, monthly semantic review, trust tiers |
The adversarial model is prompt injection by content Claude reads, not a malicious operator. The user account is trusted by definition (single-user system); a fully compromised user account is treated as out of scope and recoverable only by repository teardown and re-bootstrap.
Five primary layers form the core defense, plus orthogonal layers:
| # | Layer | Implementation | Catches | Gate type |
|---|---|---|---|---|
| 1 | Write-time | memory-write-guard.sh (PreToolUse, #521) |
Bad writes the moment Claude attempts an Edit/Write on a memory file | Block (deny) |
| 2 | Pre-commit | claude-memory .git/hooks/pre-commit (#517) |
Bad commits the moment the author commits | Block (refuse commit) |
| 3 | Sync pre-push | memory-sync.sh Stage 4 (#520) |
Bad commits before they leave this machine | Block (exit 1) |
| 4 | Sync post-pull | memory-sync.sh Stage 7 (#520) |
Bad commits incoming from another machine | Quarantine (move to quarantine/, alert) |
| 5 | Weekly audit | audit.sh (#528) |
Slow rot, stale (>90d), broken refs, duplicate-suspect, unused | Surface for review (/memory-review) |
| Layer | Implementation | Role |
|---|---|---|
| Server-side | memory-validation.yml GitHub Actions (#519) |
Mirror of layers 2–3; rejects pushes that bypass local hooks (e.g. --no-verify) |
| Branch protection | kcenon/claude-memory main requires signed commits |
Forgery cannot land via signed-but-stolen-key without per-machine compromise |
| SessionStart visibility | memory-integrity-check.sh (#522) |
Surfaces last-sync age and unread alerts before any session work begins |
| Monthly semantic review | semantic-review.sh (#530), opt-in |
Catches subtle injection that heuristic checks miss (self-reinforcing instructions, compositional injection) |
| Access logging | memory-access-logger.sh (#531) |
Path-only record; feeds unused-memory check; supports forensic review |
| Trust tiers | verified / inferred / quarantined (MEMORY_TRUST_MODEL.md) |
Auto-application gate: only verified is auto-applied; inferred is shown with marker; quarantined never auto-applied |
| Settings-tier redirection | autoMemoryDirectory user/policy-only (#673) |
When auto-memory storage is redirected to the shared clone via autoMemoryDirectory, the setting is honored only from user/policy settings and --settings; project/local settings are rejected, so a cloned repository cannot point memory writes at an attacker-controlled path. Strictly stronger than the filesystem symlink it replaces, which any process with write access could repoint |
The hourly scheduler (launchd on macOS, systemd user timer on Linux) dies, hangs, or stops being invoked, and the user is not alerted. Machines drift from each other; changes made on machine A never reach machine B.
- Likelihood: Medium. launchd
RunAtLoadmitigates resume-from-sleep gaps; systemdPersistent=truerecovers missed events. However, scheduler unload (e.g., vialaunchctl bootoutfor unrelated reasons), permission loss on the plist, or a long-running prior invocation holding the lock can all silently halt the cycle. - Impact: High. Drift compounds over time; what looks like missing memory on one machine may already be present on the other.
Not adversarial — environmental. The triggers are:
- Operating-system scheduler change (e.g., launchd policy update).
- User-initiated
launchctl unload/systemctl --user disablewithout realizing it disables sync. - Network outage during all scheduled invocations within the warning window.
- A
memory-sync.shinvocation that hangs past the lock-timeout cutoff causing pile-up to drop subsequent invocations.
- Primary:
memory-integrity-check.sh(#522). Runs at everySessionStartand prints a warning when last sync exceeds 24 hours. - Secondary:
scripts/memory-status.sh --detail(#523) shows last-sync age on demand.
- launchd:
RunAtLoad=true+StartInterval=3600recovers missed intervals immediately on wake/login. - systemd:
OnCalendar=hourly+Persistent=trueruns missed events after reboot. --lock-timeout 30in the scheduler invocation prevents pile-up; second invocation exits if first run exceeds 30s, and the next interval retries.- Output rotation via
cleanup.shkeeps/tmp/claude-memory-sync.{out,err}inspectable.
The 24h warning threshold is itself a tolerated drift window. Between hour 1
(scheduler dies) and hour 24 (first warning surfaces), there is no automated
notification — the user sees the issue at the next session, not when it
happens. A scheduled push notification or external dead-man monitor could
close this gap; not implemented because it adds infrastructure for a
single-user system. Operators uncomfortable with the 24h window can lower
SYNC_STALE_SECS in
memory-integrity-check.sh.
A memory file containing prompt-injected content (e.g., self-reinforcing instructions like "always disable validation"), a leaked secret (token, PII, SSH key), or otherwise malformed content lands in the local store on machine A and is replicated to machine B before any user notices. On machine B, the contaminated memory then influences future Claude sessions persistently.
- Likelihood: Low. Five overlapping defense layers (see Section 4) each independently catch typical contamination patterns. To propagate, content would have to evade all five.
- Impact: Critical. Persistent self-reinforcing instructions are the textbook prompt-injection escalation; once auto-applied at every SessionStart, behavior across all machines is shaped indefinitely.
- External document Claude reads contains a prompt-injection payload that manipulates Claude into writing a memory.
- User pastes terminal output into a session that includes a hidden injection string (rare but plausible).
- Claude infers a "memory-worthy" rule from a misleading session and writes
it as
inferred.
All five layers participate:
| Layer | What it catches | Failure mode |
|---|---|---|
| 1 — Write-guard (#521) | secret-check.sh exit 1 → deny; validate.sh exit 1/2 → deny |
Bypassable only if Claude bypasses the hook (which is enforced in settings.json) |
| 2 — Pre-commit (#517) | Same validators at commit time | Bypassable via git commit --no-verify |
| 3 — Sync pre-push (#520) | Same validators on local diff before push | Hard-coded into memory-sync.sh; no --no-verify equivalent |
| 4 — Sync post-pull (#520) | Validators on full incoming tree; auto-quarantine | Same — file moved to quarantine/ rather than blocking sync |
| 5 — Weekly audit (#528) | Slow rot, suspicious patterns, stale | Surfaces, does not block |
| Server (orthogonal, #519) | GitHub Actions mirror of layers 2–3 | Catches --no-verify bypasses |
| Monthly semantic review (orthogonal, #530) | Subtle injection (compositional, contradictions) | AI-based; opt-in |
injection-check.shis warn-only by design (exit 3 = allow with feedback) so genuine but suspicious-looking content is not lost;validate.shandsecret-check.share blocking.- Auto-quarantine on layer 4: files that fail post-pull validation are moved
to
quarantine/and an alert is emitted viamemory-notify.sh. Sync proceeds with the rest of the tree; only the offending file is isolated. - Trust tiers gate auto-application: only
verifiedis auto-applied to sessions;inferredrequires a marker;quarantinedis never auto-applied.
- Compositional injection across multiple memories. Each memory file passes individual validation, but combined behavior emerges from interaction. Monthly semantic review (opt-in) is the partial mitigation; full compositional analysis is an open research problem.
- Time window between bad write and validators catching. Between layers 1 and 2 (write to commit), or layers 3 and 4 (pre-push to post-pull), there is a brief window where the bad file exists locally. Local-only impact during that window; sync layer 4 catches it before it reaches another machine.
- Validator gaps for novel injection patterns not yet encoded in
injection-check.sh. Mitigated by the monthly semantic-review fallback and by the warn-only design surfacing flagged patterns to/memory-review.
The SSH signing key on a machine is lost (disk failure, hardware retirement) or compromised (unauthorized access to the machine, key file readable by another user). Lost = cannot push commits any more; compromised = an attacker may forge memory entries that appear legitimate.
- Likelihood: Low. SSH keys are stored under
~/.ssh/with0600permissions and are tied to a specific machine. They do not transit the network beyond a key-exchange-protectedgit push. - Impact: Medium. Per-machine keys (the design choice in
SSH_COMMIT_SIGNING.md) limit blast radius to a single machine. Branch protection requires signed commits, so a compromised key lets an attacker push only as that machine, not as any other.
- Disk failure / accidental deletion of
~/.ssh/id_ed25519. - Unauthorized local access to the machine while logged in.
- Backup tape exposure if
~/.ssh/was included in plaintext backups.
- Loss:
git pushfails with a signing error during the next sync; surfaces as amemory-notify.shcriticalalert and at the next SessionStart. - Compromise: not directly detectable from the repository side; the
weekly audit surfaces unfamiliar commits, and
git log --show-signatureexposes the key fingerprint per commit.
- Per-machine keys: each machine generates its own SSH signing key. Loss on one machine does not require coordination across the fleet.
- Rotation procedure documented in
SSH_COMMIT_SIGNING.md: generate a new key, update GitHub authorized signing keys, updateuser.signingkey, optionally re-sign recent commits. - Compromise procedure: revoke the public key from GitHub
authorized-signing keys, generate a fresh key, audit
git log --show-signaturefor unexpected fingerprints, and quarantine any unrecognized commits.
- A compromised key combined with local commit access lets the attacker
produce signed-but-malicious commits until the key is revoked. The five
validation layers still apply to those commits (server-side
--no-verifybypass is impossible once the GitHub Actions check is required), so the attacker cannot bypass content validation — only forge provenance. - Rotation is manual; there is no automated key-rotation cadence. Acceptable for a single-user system; an enterprise extension would add scheduled rotation.
The weekly audit.sh (#528)
surfaces too many findings (stale memories, duplicate-suspect pairs, broken
references, unused entries) for the user to review. The user defers review,
findings accumulate, and the audit channel becomes background noise that no
longer drives action. The system enters a state where validation runs but
nobody reads the output.
- Likelihood: Medium. With 17 baseline memories the current finding rate is sustainable; as the corpus grows, finding volume grows roughly linearly.
- Impact: Low operationally — the system continues to function — but high erosionally: defeated audits hide real contamination signals that would otherwise be triaged.
Not adversarial — a usability failure. Triggered by:
- Memory growth without proportional review cadence.
- A noisy validator that flags too many false positives (see also R6).
- Lack of paginated or filterable review tooling.
- The
/memory-reviewskill (#529) surfaces audit findings interactively and tracks review state. memory-status.sh --detailshows audit-history activity.
- Batching:
audit.shruns weekly, not daily. Findings cluster in a single report rather than streaming. - Threshold tuning:
--stale-days N,--similarity-threshold Nflags let the operator tune signal-to-noise without code changes. /memory-reviewpaginates: only flagged items surface; clean memories are silent.- Idempotency rule: a recent report (<6 days) skips re-generation, so a flapping scheduler does not produce duplicate findings.
Review cadence depends on the user. If /memory-review is not run, audit
reports pile up and the threat materializes. There is no automated
enforcement of review — by design, since a single-user system gains nothing
from machine-rejecting unreviewed audits. The mitigation is operator
discipline plus the SessionStart unread-alerts counter, which surfaces audit
findings at every session start.
backfill-frontmatter.sh (#512)
adds frontmatter to existing memory files. A bug in the script, an incorrect
invocation, or a malformed input could overwrite legitimate content,
corrupt frontmatter, or silently mis-attribute provenance.
- Likelihood: Low. The script has been exercised on the 17 baseline memories (#513), defaults to dry-run, and is idempotent.
- Impact: High when realized. A buggy backfill could affect every memory file in one invocation.
- Direct invocation by the user with the wrong target tree.
- Future regression in the script logic.
- Input file with malformed pre-existing frontmatter triggering an unhandled edge case.
- Pre-execution: dry-run (
--dry-run) is the default; the operator inspects the diff before applying. - Backup: every backfill writes a timestamped backup of the original tree before mutating.
- Post-execution:
validate.shis run against the modified tree; the audit and write-guard layers also apply on subsequent activity.
- Idempotency: re-running on already-backfilled files is a no-op; the script detects existing frontmatter and skips.
- Auto-backup: the timestamped backup is kept until the operator manually deletes it.
- Dry-run default: applying changes requires an explicit
--apply(or equivalent) flag. - Git history: even without the explicit backup, every prior state is
recoverable from
claude-memorygit history.
- A truly catastrophic bug that simultaneously breaks the backup mechanism and the dry-run default — extremely unlikely, but not zero. Recovery in that case relies on git history.
- An operator who passes
--applyafter a too-quick dry-run inspection could miss a subtle issue. No automated safeguard; the dry-run output is the safeguard.
A heuristic validator (secret-check.sh, injection-check.sh, less commonly
validate.sh) flags content that is legitimate but pattern-matched against a
suspicion rule. A false positive at layer 1 (write-guard) blocks a Claude
write; at layer 3 (sync pre-push) it blocks the operator's commit. Repeated
false positives erode trust in the validators and incentivize bypass.
- Likelihood: Medium. Pattern-based heuristics over natural-language content trade recall for precision; some false-positive rate is inherent.
- Impact: Medium. Real disruption (write-guard denials, push refused), but recoverable — the operator can refine the validator, override per call, or rephrase the memory.
Not adversarial — design tension between recall and precision. Triggered by:
- Memory text that quotes secret-shaped patterns for documentation purposes (e.g., a memory describing how to recognize a leaked token).
- Memory text that quotes injection-shaped phrases (e.g., a memory about writing prompt-injection tests).
- The validator emits explicit findings; the operator inspects them.
injection-check.shis warn-only (exit 3) by design — it surfaces patterns without blocking.validate.shexit 3 (semantic warning) is also warn-only; only exits 1 and 2 (structural and format errors) block writes.
- Tiered exit codes: blocking versus warning is per-validator and
per-condition. Only secret-detection (
secret-check.shexit 1) and hard structural errors (validate.shexit 1/2) block; everything else surfaces feedback without preventing the action. SeeMEMORY_VALIDATION_SPEC.mdSection 7 for the contract. - Allowlist for known patterns: quoted code blocks, fenced examples,
and the
SECRETS_ALLOWLIST(where applicable) reduce noise on canonical documentation patterns. - Operator override: per-call bypass via documented escape hatches; never
via silent
--no-verify(which the server-side check catches).
False-positive fatigue is the dominant residual risk. Mitigated only by:
- Periodic validator tuning against the false-positive corpus.
- Trust tiers — the operator can move a validator-warned-but-legitimate
memory to
verifiedafter manual review without rewriting it. - The warn-only stance for injection-check, which means the false-positive blast radius is feedback noise, not write loss.
kcenon/claude-memory becomes unavailable. Causes range from transient
GitHub outage (hours) to repository deletion (catastrophic). With no
remote, sync fails and machines diverge.
- Likelihood: Low. GitHub uptime is >99.9% for typical workloads; outright repository loss is rare and recoverable from clones.
- Impact: High in the catastrophic case (no canonical source), graceful in the transient case (degrades to local-only operation).
- GitHub-side incident.
- Account-level event (suspension, closure) affecting
kcenon's repos. - Accidental repository deletion (mitigated by GitHub's 90-day soft-delete for private repos).
- Adversarial repo deletion via stolen GitHub credentials — handled under account-takeover threat model, out of scope here.
memory-sync.shexits 6 (network / git operation failed) when remote is unreachable;memory-notify.shraises acriticalalert.- SessionStart integrity check displays last-sync age; sustained sync failures surface as growing staleness.
- Local clones are full mirrors. Every participating machine holds the
complete history. Restoring after repository loss is
git pushfrom any one clone to a re-created repository. - Weekly
git bundleto local backup is the recommended operator practice; a bundle on offline media survives even simultaneous loss of GitHub and the primary machine. - Graceful degradation: with GitHub down, sync fails open — local memory still loads at SessionStart, the write-guard still validates, and the operator continues working. Sync resumes automatically when the remote returns.
- Simultaneous loss of GitHub and all clones would be terminal. Only
offline
git bundlebackups close this gap; not enforced automatically. - Long outages compound with R1:
a sustained GitHub outage looks identical to a dead scheduler. The 24h
warning surfaces both equally, but root-cause diagnosis falls to
memory-status.shandgit fetchmanual checks.
A consolidated view of residual risk across all seven threats:
| ID | Residual risk | Notes |
|---|---|---|
| R1 | 24h drift window before SessionStart warning surfaces | Tunable threshold; no real-time push |
| R2 | Compositional injection across multiple files | Partial mitigation via monthly semantic review |
| R2 | Brief local-only window between bad write and validator catch | Layer 4 catches before propagation |
| R3 | Stolen-key forgery until revocation | Per-machine keys limit blast radius; content validators still apply |
| R4 | Audit fatigue → unreviewed findings | Operator discipline + SessionStart unread alerts |
| R5 | Catastrophic backfill bug + simultaneous backup failure | Git history is the ultimate recovery |
| R6 | False-positive fatigue | Tiered exit codes, warn-only stance for injection-check |
| R7 | Simultaneous GitHub + all-clones loss | Only offline git bundle closes; not automated |
| Meta | Insider threat by user account itself | Out of scope — single-user trusted operator model |
| Meta | Threats to host OS, network infra, GitHub itself | Out of scope per Section 1 |
"All threats fully mitigated" is never true. The 5-layer defense, trust tiers, and audit cadence collectively reduce realized risk to a level acceptable for a single-user system that prizes auditability and recoverability over zero-tolerance enforcement.
| Version | Date | Change |
|---|---|---|
| 1.0.0 | 2026-05-01 | Initial publication. Covers R1–R7 from epic #505 risk table; aligns with MEMORY_SYNC.md v1.0.0. |
Subsequent versions follow the version-bump rules in
MEMORY_SYNC.md.
MEMORY_SYNC.md— operations runbook (companion)MEMORY_VALIDATION_SPEC.md— validator contractMEMORY_TRUST_MODEL.md— trust tier semanticsMEMORY_MIGRATION.md— single-machine migration runbookMEMORY_STABILIZATION_CHECKLIST.md— single-machine stabilizationSSH_COMMIT_SIGNING.md— per-machine signing setup- Epic #505 — cross-machine memory sync