Skip to content

Security - claude-mem-house

Source material and what was found

The four scripts in scripts/ were copied from C:\Users\fives\OneDrive\ (Andrew's working copies, generated on the primary PC to hand to other house machines). The source-material pre-scan (run before this repo existed) reported no hits against the house's standard hard-gate secret regex - but a manual read during this repo's build found two real, embedded credentials that the regex does not match:

Secret Where Shape Matched the hard-gate regex?
claude-mem house API key claude-mem-client-setup.ps1, claude-mem-standby-setup.ps1 cmem_<48 hex-ish chars> No - cmem_ isn't in the pattern list
Postgres password claude-mem-standby-setup.ps1 (twice: once as a script variable, once duplicated inside a here-string that the script writes out as a second file) 30-char mixed-case alphanumeric No - arbitrary passwords have no fixed prefix/shape to match

Both are scrubbed in this repo. Neither value appears anywhere in scripts/, archive/, or tests/ - verified by:

  1. The house hard-gate regex (council_...|Bearer ...|AKIA...|sk-...|gh[pousr]_...|xox...|BEGIN...PRIVATE KEY)
  2. clean, as it was on the source files too (this regex was never going to catch either credential; see the table above).
  3. A manual grep for both exact literal values - clean.
  4. tests/Scripts.Tests.ps1 "No hardcoded house secrets" - a permanent regression test: fails the build if a cmem_[A-Za-z0-9]{20,} literal or the specific retired Postgres password ever reappears in any script, live or archived.

Remediation

Both scripts were rewritten to take the credential as a script parameter defaulting to an environment variable, matching the pilot repo's (house-council-client) established pattern for the gaming/streaming council tokens:

  • claude-mem-client-setup.ps1: -ApiKey / $env:CLAUDE_MEM_HOUSE_API_KEY
  • claude-mem-standby-setup.ps1: -PgPassword / $env:CLAUDE_MEM_HOUSE_PG_PASSWORD, -ApiKey / $env:CLAUDE_MEM_HOUSE_API_KEY

Both scripts abort with a specific, actionable error message ("Set CLAUDE_MEM_HOUSE_API_KEY... or pass -ApiKey") if the value is missing, before making any network call - verified by the behavioral tests in tests/Guards.Tests.ps1, which run the real scripts as child processes and assert both the exit code and that the health-check step was never reached.

The trickier case was the here-string in claude-mem-standby-setup.ps1 that writes a second script (~\.claude-mem\claude-mem-standby-sync.ps1, the hourly sync task) to disk on the target device. The original had the Postgres password duplicated as a literal inside that here-string - i.e. embedded twice, once directly and once inside generated code. The rewritten version has the generated sync script read $env:CLAUDE_MEM_HOUSE_PG_PASSWORD at run time, falling back to parsing the POSTGRES_PASSWORD= line out of the standby's own .env (written earlier in the same setup run, from the same operator-supplied value) - one source of truth on that device, never a literal in source control.

What is NOT treated as a secret here

  • ProjectId (a8295af4-e485-402f-9734-1f47b45b882f) - an opaque identifier for Andrew's claude-mem project, not a credential. It grants no access on its own (every /v1/* call still requires the Bearer API key); left as a literal, same as the pilot repo leaves its non-secret hostnames/ports as literals.
  • PgUser (claudemem) - a username, not a secret.
  • The primary's hostname (ANDYGREATROOMPC) and port (37877) - operational configuration, not credentials.

Non-secret findings addressed

  • BOM-safety: both claude-mem-client-setup.ps1 and claude-mem-switch-server.ps1 write settings.json via [IO.File]::WriteAllText with a BOM-less UTF8Encoding - preserved from the source scripts, which discovered (the hard way, per the source script's own comments) that PowerShell 5.1's Set-Content -Encoding utf8 adds a BOM that breaks Bun's JSON parser.
  • No -Force/silent overwrite of the primary: claude-mem-switch-server.ps1 blocks repointing the primary PC away from its own loopback stack unless -Force is passed explicitly - preserved from the source script.

Scan results summary

  • Hard-gate regex (grep -rInE "council_...|Bearer ...|AKIA...|sk-...|gh[pousr]_...|xox...|BEGIN PRIVATE KEY", full repo, .git excluded): empty.
  • Manual check for the two real credentials found in the source material: empty (verified against the exact retired literal values).
  • Codex security review (codex exec "Security review this repository for: hardcoded secrets, injection, unsafe download/exec, CI supply-chain issues", run from repo root): Not CLEAN on the first pass - no active secrets found, but real hardening findings. All addressed or explicitly accepted-and-documented below.

Codex findings and disposition

Severity Finding Disposition
HIGH Invoke-RestMethod https://bun.sh/install.ps1 \| Invoke-Expression (client-setup, standby-setup) executes unpinned remote code Accepted, documented inline - this is bun.sh's own official Windows install method; no signed/hash-pinned alternative is published upstream. Comments added at both call sites; run only interactively by a trusted operator, never from CI.
HIGH npx claude-mem@latest install runs a mutable, unpinned npm package Accepted, documented inline - deliberate (tracks upstream claude-mem closely); the standby's separate git clone already pins a commit for the parts that must match the server schema. Comment added at the call site.
HIGH pull_request CI jobs run on a non-ephemeral self-hosted runner Accepted, inherited from the mandated house CI template (matches the pilot house-council-client repo's ci.yml exactly, with the same "PR authors are trusted house members only" comment) - not something this repo can unilaterally change without diverging from the shared build brief. Tracked at the fleet level, not per-repo.
MEDIUM $PgPassword interpolated into sh -c '...' strings inside the generated sync script - a password containing ' could break out of the quoting (shell injection) Fixed. Added ConvertTo-PosixSingleQuoted in the generated sync script; PGPASSWORD='...' now uses the escaped value in all three docker compose exec ... sh -c calls. Regression-tested in tests/Scripts.Tests.ps1.
MEDIUM semgrep/semgrep CI image unpinned Accepted, inherited from the mandated CI template (the shared build brief specifies this exact unpinned form for the security job; a fleet-wide digest-pinning pass is out of scope for a single repo).
MEDIUM Install-Module Pester -SkipPublisherCheck with only a minimum-version floor Accepted, inherited from the mandated CI template (same reasoning as above).
LOW The retired Postgres password test tripwire stored the plaintext (split across a concatenation) - still reconstructable Fixed. tests/Scripts.Tests.ps1 now stores only the SHA-256 hash of the retired password and hashes password-shaped literals found in scanned files for comparison; the plaintext value does not appear anywhere in this repo, including test history from this point forward.

None of the findings were an active/live secret or a remote-code-execution path reachable without operator intent (all "unsafe download/exec" findings are intentional, interactive installer steps matching the tool's own vendor-recommended install method) - so per the house gate policy this did not require stopping and discarding work, only documenting and hardening the cheap fixes.