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:
- The house hard-gate regex (
council_...|Bearer ...|AKIA...|sk-...|gh[pousr]_...|xox...|BEGIN...PRIVATE KEY) - clean, as it was on the source files too (this regex was never going to catch either credential; see the table above).
- A manual grep for both exact literal values - clean.
tests/Scripts.Tests.ps1"No hardcoded house secrets" - a permanent regression test: fails the build if acmem_[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_KEYclaude-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.ps1andclaude-mem-switch-server.ps1writesettings.jsonvia[IO.File]::WriteAllTextwith a BOM-lessUTF8Encoding- preserved from the source scripts, which discovered (the hard way, per the source script's own comments) that PowerShell 5.1'sSet-Content -Encoding utf8adds a BOM that breaks Bun's JSON parser. - No
-Force/silent overwrite of the primary:claude-mem-switch-server.ps1blocks repointing the primary PC away from its own loopback stack unless-Forceis 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,.gitexcluded): 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.