Skip to content

fix(security): deny shell reads of tenant state + transcripts; hash login tokens at rest (v0.462.0) - #873

Merged
vikasprogrammer merged 1 commit into
mainfrom
feat/protected-read
Oct 13, 2026
Merged

vikasprogrammer merged 1 commit into
mainfrom
feat/protected-read

Conversation

@vikasprogrammer

Copy link
Copy Markdown
Owner

Why

The gate hook's protected-path check covered only Read/Glob/Grep. Agents run as the same OS user as the server, so Bash could read everything. A replay of 30 days of real commands showed this was routine:

  • expresstech: agents ran sqlite3/node:sqlite on agent-os.db 34 times. support scraped FREESCOUT_API_KEY out of other sessions' connectors/*.mcp.json. researcher grepped ~/.claude/projects for other agents' transcripts (the session-search incident).
  • Severity: auth_sessions.sid stored the login cookie verbatim, so reading the DB meant signing in as any member, owners included.

What

  • governance/protected-read.ts: an engine-level deny, folded in with stricterDecision, so it applies on every tenant whatever its persisted policy. It fires when a shell call names a protected path in the command, a heredoc or -e body, or a script it executes. Relative paths through the agent folder or a cd, ~/$HOME, globs, WAL sidecars, and recursive readers on an ancestor are all caught.
    • Covered: the DB, secret.key, connectors/, control/, tenants/, runtime-accounts/, claude-config/, ~/.claude/projects, and the Claude/Codex logins.
    • Deliberately not covered: ~/.ssh and ~/.aws. In a first draft, 88 of 120 hits were infra agents using deploy keys by design.
    • Exempt: the agent's own transcript folder, Claude Code auto-memory, and skill/plugin folders.
    • Deny message: points at session_history or secret_get/secret_request.
    • Policy preview: policy_check gives the same answer as the gate.
  • Tokens at rest: auth_sessions.sid and invites.token are stored as SHA-256 (h1: tag). The migration hashes existing rows in place, so cookies and links already handed out keep working.
  • Read-tool list: secret.key is added (it was missing).

Measured

  • Replay, final rules: instapods 17 hits in 14,299 commands, and expresstech about 60 in 10,929. All but two are true positives; the two are ~/.claude.json mentioned inside code edits.
  • Cost on the gate's hot path: p50 0.06ms, p99 0.6ms, max 2.1ms. macOS autofs paths (/home, /net) are resolved lexically, because one stat cost 318ms.

Limits

This is an intent check. A deliberately obfuscated path gets past it, and the real wall is AOS_UID_ISOLATION. Hashed tokens mean a read that does get through can't sign anyone in.

Tests

New protected-read-test covers the incident's exact shapes, the bypass variations, the allowed cases, the end-to-end gate and preview, and token hashing including migration and idempotence. The full governance suite passes.

🤖 Generated with Claude Code

…t key and other sessions' transcripts; hash login tokens at rest (v0.462.0)

The gate hook's protected-path arm only covered Read/Glob/Grep; Bash, running as the server's OS user,
reached everything. Replaying 30 days of real commands: on expresstech agents queried agent-os.db 34
times, scraped FREESCOUT_API_KEY from other sessions' connectors/*.mcp.json, and grepped
~/.claude/projects for other agents' transcripts (the session-search incident).

- governance/protected-read.ts: engine-level deny (stricterDecision, every tenant) when a shell call
  names a protected path in the command, a heredoc/-e body, or a script it runs; relative paths via the
  agent folder or cd, ~/$HOME, globs, WAL sidecars, recursive readers on an ancestor. Covers the DB,
  secret.key, connectors/, control/, tenants/, runtime-accounts/, claude-config/, ~/.claude/projects and
  the Claude/Codex logins; NOT ~/.ssh/~/.aws (infra agents use keys by design: 88/120 hits in a first
  draft). Own transcript folder, auto-memory and skill/plugin dirs exempt. Steers to session_history /
  secret_get / secret_request. Policy preview agrees. p99 0.6ms per command; macOS autofs paths resolved
  lexically (a /home stat cost 318ms).
- auth_sessions.sid and invites.token stored as sha256 (h1: tag); migration hashes existing rows in
  place, so issued cookies/links keep working.
- secret.key added to the Read tool's protected list (was missing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vikasprogrammer
vikasprogrammer merged commit 0c92943 into main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant