security.md — audit surface + measured state
Snapshot of every credential-touching surface in snappy-os mini. Measured 2026-04-18 by the Pod P security audit. Re-run the probes below before calling any row "still green."
1. Credential file — .env.cache
- Canonical path:
~/projects/snappy-os/.env.cache(program.md § Credentials). - Back-compat symlink:
~/.claude/skills/snappy-settings/.env.cache→ canonical. - Permissions (measured):
600(owner read/write only). Pass. - Git tracking (measured): not tracked; listed in
.gitignore:2. Pass. - Git history scan:
git log --all -p | grep -iE '(sk-[a-zA-Z0-9]{20,}|xoxb-|AIza[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16})'returned zero hits. Pass. - Known quirk: the back-compat symlink was missing on this box during the audit (
os error 2). Fix:
mkdir -p ~/.claude/skills/snappy-settings
ln -sf ~/projects/snappy-os/.env.cache ~/.claude/skills/snappy-settings/.env.cache
Kernel-era scripts that still open the old path will silently fail until this is restored. The audit pod lacked permission to run mkdir -p ~/.claude/...; operator must do this once per machine. Logged as a P1 friction (see end of file).
2. Other on-disk credential caches
| Path | Perms | Tracked? | Notes |
|---|---|---|---|
.linkedin-token-cache.json | 600 | gitignored (.gitignore:3) | LinkedIn OAuth refresh cache, owner-only. |
state/log/ | dir 755, files 644 | gitignored (.gitignore:11) | Logs, never contains raw tokens; byline-keys.log is keystroke samples, not secrets. |
state/agents/ | dir 755 | gitignored (.gitignore:18) | Per-machine agent state (ticker/status), no secrets. |
state/directive.json | n/a | gitignored (.gitignore:23) | Legacy single-agent state, not present on this box. |
No credential-shaped filenames are tracked in git (grep of git ls-files against *token*, *cred*, *secret*, *.key, *.pem returned nothing). Pass.
3. .gitignore coverage
Current entries (.gitignore, 22 lines):
node_modules/,.env.cache,.linkedin-token-cache.json*.tgz,.bootstrap-report.jsonstate/log/,state/log/regen-queue/,state/catalog/state/agents/,state/directive.json
No missing sensitive paths identified. The one marginal case is state/tmp/ — not gitignored, but currently contains only content-mine-run.ts (code, not secrets). Leave as-is; add a rule only if ad-hoc token dumps start landing there.
4. Transport — HTTP vs HTTPS
Scanned bin/ and state/lib/ for http://:
bin/— zero plaintext HTTP calls. Allhttps.request/https.get.state/lib/— ten hits, all of them localhost or LAN diagnostics (linkedin.ts:565OAuth loopback,youtube.ts:314OAuth loopback,remotion.ts:582local studio,box.ts:28Mac Mini LAN,maintenance.ts:32local healthcheck,walkthrough.tsSVG xmlns literals). None ship credentials over plaintext to an external host. Pass.
Master-key transport:
bin/cli.js:62targetshttps://skills.snappy.ai.bin/cli.js:302-304attachesAuthorization: Bearer ${SNAPPY_MASTER_KEY}only on HTTPS Worker paths.bin/do-spaces.jsusesimport https from 'node:https'exclusively; SigV4 signatures travel over TLS.
Pass — no plaintext credential transport.
5. Hook trust model (~/.claude/hooks/snappy-os-inject.sh)
What ships:
- Repo-of-truth:
~/projects/snappy-os/hooks/snappy-os-inject.sh(167 lines, bash). - Installed-on-user:
~/.claude/hooks/snappy-os-inject.sh— identical (diffreturned empty). - Bootstrap path:
curl -sS https://skills.snappy.ai/.well-known/skills/snappy-os/hooks/snappy-os-inject.sh -o ~/.claude/hooks/snappy-os-inject.sh && chmod +x(program.md § Self-bootstrap, step 2).
What is verified:
- TLS only (
https://skills.snappy.ai, HTTP/2 confirmed viacurl -sSI). - Cloudflare Worker in front of DO Spaces (
x-snappy-source: doheader). Authorization: Bearer ${SNAPPY_MASTER_KEY}on personal-tier paths; public paths (hook body, program.md, state/index.md) return 200 without auth.- Direct bucket path
https://robert-storage.tor1.digitaloceanspaces.com/snappy-os/hooks/snappy-os-inject.shreturns 403. Good: bucket is not publicly readable at origin; the Worker is the only ingress.
What is not verified:
- No signature check. The install step is
curl ... | chmod +x; there is no GPG detached-signature, no minisign, no SHA pin against a value stored in git. A gateway compromise (Cloudflare-Worker takeover, DO Spaces key leak, BGP hijack with mis-issued TLS cert) would let an attacker ship arbitrary bash into every Joe machine the next time they re-install the hook. The hook runs onUserPromptSubmitandPreToolUse:Task, so arbitrary code would execute inside every Claude Code session. - The runtime fetch at
snappy-os-inject.sh:64pullsprogram.mdandstate/index.mdover the same gateway and injects the content into prompts. This is less dangerous than the install path (the fetched content is text, not exec'd), but a poisoned program.md could still steer the agent toward unsafe actions. Mitigation: the hook falls back to${LOCAL_FALLBACK}/program.md(local repo) when network/gateway is unavailable, so an operator who suspects compromise can block egress and keep running.
Trust boundary: the SSL/CF/DO stack and the repo push path. Both are single-operator today (Robert). Adding a signature pin is cheap and closes the install-path hole — proposed but not shipped in this audit (out of scope per mission: "document the risk," not "rotate trust roots").
6. DO Spaces bucket posture
- Bucket:
s3://robert-storage/snappy-os/(DO regiontor1). - Signer:
bin/do-spaces.js, pure-Node SigV4, readsDO_SPACES_KEY/DO_SPACES_SECRETfrom env. - Direct origin access:
HEAD https://robert-storage.tor1.digitaloceanspaces.com/snappy-os/hooks/snappy-os-inject.sh→403(bucket is not public-read at origin). - Gateway access:
HEAD https://skills.snappy.ai/.well-known/skills/snappy-os/hooks/snappy-os-inject.sh→200withx-snappy-source: do. Worker holds the DO creds and proxies. - Who holds keys: program.md § Sync states "Joe machines never hold DO credentials; the Worker is the only ingress." Verified —
DO_SPACES_KEY/SECRETare only referenced inbin/do-spaces.jsand that file is a Worker-side utility.env.tswould throw on a Joe machine that tried to call it directly.
Pass, with the caveat in § 5 (no signature on the hook body served by that gateway).
7. Audit evidence (reproducible probes)
# 1. env.cache perms
ls -la ~/projects/snappy-os/.env.cache
# 2. git history for secret values
cd ~/projects/snappy-os
git log --all -p | grep -iE '(sk-[a-zA-Z0-9]{20,}|xoxb-|AIza[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{20,}|AKIA[A-Z0-9]{16})'
# 3. gitignore coverage
cat .gitignore
# 4. transport scan
grep -rn 'http://' bin/ state/lib/
# 5. hook diff
diff ~/projects/snappy-os/hooks/snappy-os-inject.sh ~/.claude/hooks/snappy-os-inject.sh
# 6. bucket direct vs gateway
curl -sSI https://robert-storage.tor1.digitaloceanspaces.com/snappy-os/hooks/snappy-os-inject.sh | head -1
curl -sSI https://skills.snappy.ai/.well-known/skills/snappy-os/hooks/snappy-os-inject.sh | head -1
8. Recommended follow-ups (not shipped in this audit)
All three need Robert's explicit approval — out of scope per Pod P mission.
- Signature-pin the install path. Emit a
sha256:...into.bootstrap-versionat publish time; haveinstall.jsverify the fetchedsnappy-os-inject.shhash matches beforechmod +x. Closes the gateway-compromise-arbitrary-bash window described in § 5. - Restore missing back-compat symlink on this box. Command in § 1. Not fatal today (no current caller hit the missing path during this audit), but the next kernel-era script that does will fail silently. Logged as P1 friction.
- Rotate
SNAPPY_MASTER_KEY. The key was never found in git history, but key rotation is cheap insurance after any audit. Skillsync-creds(state/skills/sync-creds.md) owns this verb.
9. Out-of-scope (explicitly not audited here)
- Client-owned creds in kernel directories (
~/.claude/skills/snappy-*/). Kernel is frozen reference; auditing its individual skill sandboxes is a separate pod. - Upstream CF Worker code (
snappy-os-workerrepo). The Worker is the security-critical hop; its code lives outside this repo and needs its own review. .npmrc/ publish tokens. Per the/snappy-releaseautonomy contract, npm 2FA bypass tokens live in.env.cache; handling those is the release skill's job, not this audit.
Friction (logged 2026-04-18)
P1— back-compat~/.claude/skills/snappy-settings/.env.cachesymlink missing on Robert's laptop. Kernel-era consumers will silently fail. Fix: 2-linemkdir -p && ln -sfin § 1. Pod P lacked permission to run outside repo; operator action required.