# Security and Code Quality Audit: Sibyl-Memory

- **Repo**: https://github.com/Sibyl-Labs/Sibyl-Memory
- **Commit audited**: `39cb495` (release: sibyl-memory-client 0.4.10)
- **Date**: 2026-06-09
- **Scope**: all 5 packages (client, mcp, cli, hermes plugin, sql schema), ~9.2k lines of source excluding tests, plus CI workflow, dependencies, git history and README claims.
- **Method**: 4 parallel review passes (client internals, MCP server + CLI, Hermes plugin + credentials, supply chain) with manual verification of the load-bearing findings (`_capcheck.py` network payload, `hf-mirror.yml`).

## Verdict

Well-hardened codebase, clearly above average for a project this young (created 2026-05-20). Supply chain is trustworthy: zero runtime dependencies in the client, no install hooks, no dynamic code execution, no committed secrets. The issues found are fixable in isolation; none are architectural.

**Found: 1 high, 2 medium, 1 low, plus 1 documentation suggestion.**

## Strengths

- **Zero runtime dependencies in the client** (stdlib only), no git/URL deps, no install hooks, no `eval`/`exec`/`pickle` anywhere in the repo. Clean supply chain.
- **Fully parameterized SQL**, with FTS table names guarded by frozenset allowlists (`_EXTERNAL_CONTENT_FTS`).
- **Hardened credential handling**: `os.open(O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0o600)` eliminates the TOCTOU window on file mode, symlink detection before `.resolve()` (SEC-11), parent dir created 0700, tokens never logged in clear text (redaction helper in cli).
- **Real prompt-injection mitigation** in the Hermes adapter: prefetched memory content is wrapped in an explicit untrusted-context fence, and trimming applies to the body only so the closing fence always survives (covered by `test_prefetch_fence.py`). Tool exceptions return only the exception class name to the model; full traces stay in local logs.
- **Install safety**: plugin files are copied from the package via importlib.resources (no network fetch at install or runtime), sentinel check (`name: sibyl` in plugin.yaml) before any `rmtree`, refuses to install through symlinks.
- **Sane offline behavior in the cap gate**: 7-day grace cache, and the T2-3 fix correctly avoids downgrading paid users to free tier on transient 5xx responses.
- **No memory content ever leaves the machine**: the check-write payload carries only `account_id`, `session_token`, current DB size and proposed delta bytes (verified by reading `_capcheck.py:373-386`). The tier check is license gating, not telemetry.
- **MCP hardening**: pydantic validation-error scrubbing prevents raw argument values (potential secrets) from being echoed back through the protocol (SEC-14, tested in `test_arg_validation_leak_2026_06_02.py`); limit values clamped server-side.
- No committed secrets (grep for common token patterns clean), thorough `.gitignore`, descriptive commit history, README claims match what the code actually does.

## Findings

### HIGH: silent config destruction on corrupted settings (sibyl-memory-cli)

`sibyl-memory-cli/src/sibyl_memory_cli/setup.py:493-508`, `_write_settings_with_sibyl()`.

If `~/.claude/settings.json` contains invalid JSON, the `JSONDecodeError` is swallowed, `cfg = {}`, and the file is rewritten containing only the sibyl-memory MCP block. A `.bak` backup is created beforehand, but the user is never told that their existing configuration (other MCP servers, theme, settings) was discarded.

Same pattern for the Hermes YAML config in `setup.py:218-232`, `_write_config_with_sibyl()`: a malformed `config.yaml` silently reinitializes to an empty dict.

**Suggested fix**: fail fast on parse errors with a clear message pointing to the file, or at minimum print an explicit warning that the original config was unreadable and where the backup lives. Silent reinitialization should never happen.

### MEDIUM: `tiers` parameter not validated in memory_search (sibyl-memory-mcp)

`sibyl-memory-mcp/src/sibyl_memory_mcp/server.py:348-352`.

The CSV `tiers` string is split and stripped, but each item is not checked against the allowed tier names ("entity", "state", "reference", "journal") before being passed to `client.search()`. No SQL injection is possible (the client uses allowlists downstream), but an invalid tier name produces undefined behavior instead of a clean error.

**Suggested fix**: whitelist-validate each tier and reject unknown values with an explicit ValidationError.

### MEDIUM: deprecated naive `datetime.utcnow()` (sibyl-memory-client)

`sibyl-memory-client/src/sibyl_memory_client/lint.py:261` and `:351`.

`datetime.utcnow()` is deprecated since Python 3.12 and scheduled for removal in 3.14, and it returns a naive datetime. Comparisons against the timezone-aware UTC timestamps used everywhere else (the `_utc_now_iso()` pattern) can silently misbehave, affecting stale-entity detection and flagged-actor recency checks.

**Suggested fix**: `datetime.now(timezone.utc)` in both spots.

### LOW: HF_TOKEN inline in the mirror push URL (CI)

`.github/workflows/hf-mirror.yml:47`.

The mirror push uses `https://sibyllabs:${HF_TOKEN}@huggingface.co/...`. GitHub Actions masks secrets in logs and the script does not echo the URL, so actual exposure risk is low, but git error messages can sometimes include the remote URL. A git credential helper or an SSH deploy key would remove the failure mode entirely.

### Suggestion (docs, not a bug): disclose the tier-check API call

The "Local SQLite" positioning is accurate for memory content, but the paid-tier cap check does call `https://api.sibyllabs.org/api/plugin/check-write` when an activated account writes past its cap. A one-line disclosure in the README ("tier verification calls our API with account metadata only, never memory content") would preempt the question from privacy-conscious users and turn a potential trust concern into a selling point.

## Areas checked and found clean

- SQL injection (parameterized everywhere, identifier validation for entity/state keys, null byte and control character rejection)
- Command injection (all subprocess calls use list form, no `shell=True`, hardcoded package names)
- Path traversal (backup paths validated via `Path.relative_to()`)
- Secrets in git history and working tree
- Dependency hygiene across all 4 `pyproject.toml` files (no typosquats, no URL deps, version floors set)
- Outbound network surface (only `api.sibyllabs.org` for tier verification, nothing else)
- Atomic config writes (write-to-temp + `os.replace()` across JSON/YAML/TOML)
- Multi-tenant isolation (tenant_id on all tables with UNIQUE constraints, DEFAULT_TENANT fallback pre-activation)
