Skip to content

Keep credentials out of what the agent stores, shows and reaches - #8

Merged
stawan15 merged 2 commits into
mainfrom
feat/usage-ledger
Nov 1, 2026
Merged

stawan15 merged 2 commits into
mainfrom
feat/usage-ledger

Conversation

@stawan15

@stawan15 stawan15 commented Nov 1, 2026

Copy link
Copy Markdown
Owner

The agent reads untrusted things — a web page, a dependency, a reply — and then acts with the user's account behind it. This closes the places where that turns into a credential leak, and tightens the phone page.

What leaves the process is masked

Conversations, prompt history, exports, the phone page and resume-search snippets pass through a redactor. It catches the key shapes worth knowing — sk-, sk-ant-, ghp_, glpat-, xoxb-, AKIA, AIza, JWT, PEM blocks, Bearer …, user:pass@, password=, quoted JSON values — and the exact keys in auth.json, the provider environment and the phone token. The last four characters survive so a person can tell which key leaked: sk-[redacted:…3456]. RedactJSON keeps a tool call's JSON valid, and a second pass doesn't touch a marker it already wrote. /scrub cleans the conversations saved before this existed.

Child processes no longer inherit the shell's keys

Bash, background jobs, hooks and MCP servers get an environment with *_API_KEY, *_TOKEN, *_SECRET, *_PASSWORD, AWS_* and similar dropped, so a model that runs env sees PATH, not credentials. An MCP server still gets exactly the variables its own configuration lists. A shell command that names a credential store (~/.ssh, ~/.aws, ~/.git-credentials, teveus's config dir, cloud metadata) asks first even in autopilot, because a command has no paths to inspect before it runs.

WebFetch can't reach this machine

Loopback, private, link-local, CGNAT and cloud metadata are refused on the first request and on every redirect, and the address actually connected to is checked, so a DNS answer can't change in between. WebFetchLocal turns that off for a local dev server.

The phone page

  • Listens on 127.0.0.1 unless you name another address; binding wider still works and now warns that the page is plain HTTP.
  • Trades the token in its link for an HttpOnly, SameSite=Strict cookie and redirects to /, so the address bar, the history and the page's own scripts stop carrying it.
  • Sends a nonce-based CSP and the usual frame, sniffing and referrer headers; bounds timeouts, header size and body size.
  • Can make permissions stricter but not looser: autopilot and "always allow" stay in the terminal. So do trusting a folder and agreeing to run Claude Code — the page says what's waiting and points at the terminal.
  • Other windows' pages no longer carry their links; tapping one asks this server to open it.
  • A masked field's value never reaches the page.

Undo

/undo and /rewind give a file back its original permissions instead of a fixed 0644, so a restored 0600 key file doesn't become world-readable.

Docs and tests

SECURITY.md, PRIVACY.md, README.md and CHANGELOG.md describe the new behaviour (the changelog is regenerated into docs/changelog.json). New tests cover every redaction shape and its idempotency, save and scrub, env filtering, the SSRF guard including 100.64/10 and redirects, the credential ask in autopilot, the cookie flow, the security headers, mode and permission escalation, local-only questions and masked input.

The agent reads untrusted things — a web page, a dependency, a reply — and
acts with the user's account behind it. This closes the places where that
turned into a credential leak.

What leaves the process is now masked. Conversations, prompt history,
exports, the phone page and resume-search snippets pass through a redactor
that catches the key shapes worth knowing (sk-, ghp_, AKIA, PEM blocks,
Bearer, user:pass@, password=, quoted JSON values), plus the exact keys in
auth.json, the provider environment and the phone token. The last four
characters survive so a person can tell which key leaked. /scrub cleans the
conversations saved before this.

Child processes no longer inherit the shell's keys: Bash, background jobs,
hooks and MCP servers get an environment with *_API_KEY, *_TOKEN, *_SECRET,
*_PASSWORD and AWS_* dropped, while an MCP server still gets exactly the
variables its own configuration lists. A shell command that names a
credential store asks first even in autopilot, because a command has no
paths to inspect before it runs.

WebFetch refuses loopback, private, link-local, CGNAT and cloud metadata on
the first request and on every redirect, and checks the address it actually
connected to, so a DNS answer can't change in between. WebFetchLocal turns
that off for a local dev server.

The phone page listens on 127.0.0.1 unless the user names another address,
trades the token in its link for an HttpOnly SameSite=Strict cookie and
redirects to /, sends a nonce-based CSP and the usual frame and sniffing
headers, and bounds its timeouts and header size. It can make permissions
stricter but not looser: autopilot and "always allow" stay in the terminal,
and so do trusting a folder and agreeing to run Claude Code. Other windows'
pages no longer carry their links; tapping one asks this server to open it.

/undo and /rewind give a file back its original permissions instead of a
fixed 0644, so a restored 0600 key file doesn't become world-readable.
staticcheck read the bool comparison in phoneIsPublicAddr as noise; and
Windows has no Unix permission bits, so the undo test has nothing to
assert there.
@stawan15
stawan15 merged commit f22387c into main Nov 1, 2026
4 checks passed
@stawan15
stawan15 deleted the feat/usage-ledger branch November 1, 2026 05:56
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