[Security] No sanctioned channel to hand Claude a secret — a map of 18 open requests and the minimal primitive that unblocks them
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Summary
Claude Code has no sanctioned way for a user to hand it a secret. The only channel available is the chat prompt, and the chat prompt is the transcript — which persists in three separate on-disk stores, is re-sent as context on every subsequent turn, and can reach five-year retention through /bug.
That single gap is generating a continuous stream of issues. 18 open issues, zero shipped, the oldest from February 2026, three filed in the last week alone. Each one treats a symptom; none can close the gap, because the gap is an input channel and only the core owns the input.
The ask is one tool: an interactive, masked secret prompt whose value never enters the transcript or the model's context. In the preferred form it adds no config, no providers, no persistence and no new storage — the value lives in process memory for the session and is gone at exit.
---
Why prevention is the only control that actually executes
This is not a convenience request, and it is not another wishlist item. It follows from a chain I would ask reviewers to break if they can:
- Users will keep handing secrets to agents. This is not carelessness to be trained away — it is the natural consequence of an agent that acts on your behalf. The moment a task needs a token, the user has one impulse and one available surface: the prompt box.
- Therefore prohibition does not work.
CLAUDE.mdrules, docs and warnings are advisory and lose to urgency. #82796 is a filed report of the model itself violating an explicit "never print secrets" instruction. Guidance is not a control. - So only two controls remain: prevent the write, or remediate afterwards.
- Remediation is rotation, and rotation does not happen. 64% of valid secrets leaked in 2022 were still unrevoked four years later. Rotation is tedious, it breaks running systems, and it requires the user to first realise they were exposed — which the current design actively hides, since the leak leaves no visible mark. Worse, once the value is in a transcript, the only honest advice is "rotate it", and the user has to re-enter the new secret through the same leaky channel. The remedy re-creates the problem.
- Therefore the only control that executes is preventing the write.
The design consequence is that the safe path has to be available at the moment of the impulse, and it has to cost the user nothing. A safe path that requires prior setup — a vault, a synced store, a provider account — is not available at that moment and loses to the prompt box every time. That is precisely why the primitive has to be a zero-configuration prompt inside the session, and why it cannot be another piece of infrastructure the user has to adopt first.
---
The pattern: 18 open issues, none shipped
Getting a secret in — different users, same missing primitive:
| Issue | Opened | What it asks for |
|---|---|---|
| #23642 | 2026-02-06 | 1Password op:// references in settings.json |
| #29910 | 2026-03-01 | built-in secrets management with third-party backends |
| #32733 | 2026-03-10 | secure secrets injection for Claude Code on the web |
| #73582 | 2026-07-02 | store credentials in the OS secret store, not plaintext |
| #77084 | 2026-07-13 | secret lockbox with broker injection — "agent can USE secrets, never READ them" |
| #88165 | 2026-08-20 | secrets locker so fields can be filled without the model seeing values (a11y) |
| #88757 | 2026-08-22 | envHelper to supply env vars for config expansion |
| #89706 | 2026-08-26 | opt-in to let Claude Code write secret values to credential stores |
| #90116 | 2026-08-27 | non-developers cannot get a working local env securely |
Closed without a solution: #38797 (masked input — auto-closed as a duplicate of #29910 three days after filing), #44158 (${input:id} masked prompts, not planned), #33654 (OS keychain for MCP config, not planned), #2695 (zero-trust env vars, not planned).
Keeping a secret from getting out — the same gap seen from the other side:
| Issue | Opened | What it reports |
|---|---|---|
| #44868, #58043, #59094 | Apr–May 2026 | agents read .env / credential files straight into the transcript — all three closed as not planned |
| #71654 | 2026-06-26 | has repro: live GitHub PAT and an admin Forgejo token written to transcripts; both burned |
| #72156 | 2026-06-28 | /feedback submissions need secret scrubbing before upload |
| #80153 | 2026-07-22 | redact sensitive env-var values from Bash output |
| #82351, #87838 | Jul–Aug 2026 | claude mcp get/list print secret headers in cleartext |
| #82796 | 2026-07-31 | model repeatedly violates an explicit "never print secrets" instruction |
| #88380 | 2026-08-20 | detect and offer to mask secrets in pasted text before submission |
| #90010 | 2026-08-27 | security-guidance echoes raw OAuth tokens from .credentials.json |
The count, stated precisely (verified against the GitHub API on 2026-08-28): 18 of the issues referenced above are open — nine on the inbound side, eight on the outbound side, plus #78344 below. Eight more were closed without a solution (#38797 as a duplicate; #44158, #33654, #2695, #29434, #44868, #58043, #59094 as not planned). None was closed as completed. Twenty-six issues, six months, zero mechanism.
Two observations. First, the outbound issues keep arriving because there is nothing upstream to stop the secret from being there in the first place. Second, the instruction-based mitigations fail by design: #82796 is literally a report that prose rules do not hold.
---
Root cause: every one of these is downstream of a missing input channel
The requests split cleanly in two. Outbound issues ask Claude Code to stop showing a secret it already has. Inbound issues ask for somewhere to put a secret in the first place. All the engineering attention so far has gone to the outbound side — redaction on /feedback upload, redaction in error reports, permission denials on .env reads.
But a secret only needs redacting because it was allowed in. And the way it gets in is the prompt box, because that is the only door in the building.
CLAUDE.md rules do not close it — they are advisory, they lose to urgency, and #82796 documents them losing. Hooks do not close it either (details under Alternatives). Only the core can, because the core owns the input field and the transcript.
---
The double bind: the unsanctioned path both leaks and fails
This is the part I would most like the team to look at, because it shows the gap costs users twice.
#78344: a user pasted their own Vercel API token into chat — the only way to give it to Claude — to add one environment variable to their own project. The auto-mode classifier then blocked five different legitimate ways to use it: an Edit to their own source file, a curl to Vercel's official API, npx vercel env add with the value piped from stdin, and a write to the standard Vercel CLI auth.json. All five denied with Blocked by classifier. A one-minute task took ninety, and, in the reporter's words, "zero safety gained".
So the current state is: the only available channel is the one that leaks, and after paying that price the user still cannot complete the task. The classifier is not wrong to be suspicious — a raw credential interpolated into a shell command is genuinely indistinguishable from exfiltration. It has nothing else to go on.
A secret broker fixes that too. A command that declares uses: ["VERCEL_TOKEN"] and receives the value through the environment is machine-distinguishable from a command with a credential pasted into its argument string. The permission layer finally has a signal it can allow on, instead of a shape it must deny. This is a benefit to the platform's own safety machinery, not just to user convenience.
---
Evidence: where a pasted secret actually lands
Verified on Claude Code 2.1.247, macOS. A secret pasted into chat is written to at least three places on disk, not one:
| Path | Contents |
|---|---|
| ~/.claude/projects/<project>/<session>.jsonl | session transcript, plaintext |
| ~/.claude/history.jsonl | prompt history — display and pastedContents fields |
| ~/.claude/paste-cache/*.txt | individual files holding pasted text |
Reproduce with a harmless marker rather than a real credential:
# paste MARKER_STRING into the chat prompt, then:
grep -rl "MARKER_STRING" ~/.claude/projects/ ~/.claude/history.jsonl ~/.claude/paste-cache/
I have not seen this noted in any of the 18 issues: deleting a session does not remove the secret from disk. A user who discovers the exposure and clears their history is left with copies in two other stores and false confidence that they cleaned up.
Retention, per current documentation: local transcripts are plaintext for 30 days by default (cleanupPeriodDays); /feedback, /bug and /share send a copy of the conversation to Anthropic with 5-year retention and optionally open a public GitHub issue; consumer accounts with model improvement enabled are also at 5 years. One /bug from a session containing a live token puts a working credential into five-year retention.
Classification: CWE-532 (Insertion of Sensitive Information into Log File — a transcript is functionally a log), CWE-312 (Cleartext Storage of Sensitive Information — the .jsonl files are plaintext), and OWASP LLM02:2025 Sensitive Information Disclosure, which names security credentials explicitly among the categories at risk.
A related weakness is what makes the workaround unsafe too: CWE-214, Invocation of Process Using Visible Sensitive Information — "a process is invoked with sensitive command-line arguments, environment variables, or other elements that can be seen by other processes on the operating system." Today the model's only option is to interpolate the credential into a shell command, which is exactly that pattern. The design below deliberately avoids the command line for this reason.
Scale, measured independently. GitGuardian's State of Secrets Sprawl 2026 reports "28.65 million new hardcoded secrets were added to public GitHub commits in 2025 alone, a 34% increase year over year", and AI-service secrets "reached 1,275,105, up 81% year over year".
Two figures from that report bear directly on this issue.
Claude Code is named in the data. The report states: "Claude Code-assisted commits showed a 3.2% secret-leak rate, versus a 1.5% baseline across all public GitHub commits." Commits made with this product leak secrets at more than twice the rate of GitHub as a whole. Whatever the mix of causes, that is an external, quantified signal about secret hygiene around this specific tool.
Rotation does not happen. The same report finds that of credentials confirmed valid in 2022, "nearly 70%" were still valid in January 2025, and "the validity rate was still above 64%" in January 2026. Telling users to rotate after the fact is not a control — four years later, most of them have not. Protection has to work before the paste.
Proposed Solution
Why this is not a duplicate of the issues above
I am asking specifically that this not be auto-closed against #29910, which is what happened to #38797 — the one prior issue that asked for exactly this primitive — three days after it was filed.
- #29910 proposes secrets-management infrastructure: a committed
secrets.json, pluggable backends (Doppler, 1Password, AWS/GCP), aclaude-secrets run --wrapper, persistence, team sharing. That is a peer to Doppler and direnv, and it is a large, long-lived surface with third-party integrations to maintain. - #77084 is a synced, cross-device lockbox aimed at multi-machine and headless
claude -pworkflows. - #88165 is a persistent locker for repeated form-filling, driven by an accessibility need.
- #88380 is detection of credential-shaped text in a paste — a safety net for when the user has already decided to paste.
This request is none of those. It is the input primitive all four assume but none of them can be reduced to: a masked field whose value goes to the execution environment and never to the transcript or the model. Its nearest relative is AskUserQuestion, not a secret manager. Nothing is configured in advance, nothing persists past the session, nothing is synced or shared, and no new config file is introduced.
The decisive distinction is buildability. #29910, #77084 and #88165 can all be built outside the core — a store, provider integrations and a run -- wrapper work fine as an MCP server or a standalone CLI, and several commenters on #29910 have built exactly that. A safe input channel cannot be built outside the core: the masked field and the transcript belong to Claude Code. Anything an external component collects has to pass through the session to get back in.
That is the line that decides what belongs in the core. This primitive falls on the core side; the infrastructure proposals do not.
It is also the cheap half. #29910 is quarters of work and a permanent integration-maintenance burden. This is a tool, an interactive secret prompt of the kind the CLI already ships — claude mcp add --client-secret is documented in --help as "Prompt for OAuth client secret (or set MCP_CLIENT_SECRET env var)", i.e. the CLI already declines to take that value on the command line — and an environment injection at spawn time.
---
This follows Anthropic's own guidance
From Securely deploying AI agents:
Rather than giving an agent direct access to an API key, you could run a proxy outside the agent's environment that injects the key into requests. The agent can make API calls, but it never sees the credential itself.
And in the least-privilege table: "Credentials — inject via proxy rather than exposing directly."
The same document notes that "a developer running Claude Code on their laptop has different requirements than a company processing customer data" — and then offers that developer nothing. Envoy, containers and TLS-terminating proxies do not apply to a laptop. The only channel actually available there is the chat prompt: the exact opposite of the documented recommendation.
This request applies the same principle at laptop scale — the agent uses the credential but never sees it — and it removes one leg of the lethal trifecta the documentation links to. With the value outside the context window, a successful prompt injection cannot make the model disclose what it never had.
Prior art: a commenter on #29910 documents that Manus already ships this pattern — an inline input card, the value shown as •••• beside its key name in the conversation, never transiting the model. The interaction is validated in production by a competing agent.
---
Proposed solution
Option 1 — preferred: memory only
AskUserSecret({ name, purpose }). The model calls it instead of asking in chat. The tool description carries the rule: never request a secret in the conversation, call this instead. (This part matters — without it the tool is dead weight, since the failure mode today is the model politely asking for a paste.)- A deliberately conspicuous panel. Same shape as
AskUserQuestion, but visually distinct, because handing over a credential should read as a notable event rather than a routine question. Input masked, terminal echo off. - The value is held in process memory (
Buffer) for the session. It never reaches the transcript, the prompt history, the paste-cache, or the model's context. - The model receives metadata only:
``json``
{ "name": "GITHUB_TOKEN", "stored": true, "length": 40, "fingerprint": "a3f1c8e2" }
Name, length and fingerprint are enough to tell secrets apart and confirm the right one is loaded.
- Injection at spawn time. The Bash tool declares
uses: ["GITHUB_TOKEN"]; Claude Code puts the value into the child process environment as it spawns. The transcript records the script text with the variable name. The secret is never interpolated into a command string — that would put it in the transcript before execution. - Zeroed at session end. The secret does not survive a restart. That is part of the protection, not a limitation.
╭─ 🔑 Claude is requesting a secret ─────────────────────────╮
│ │
│ Name: GITHUB_TOKEN │
│ Purpose: call api.github.com on your behalf │
│ Storage: this session only, wiped on exit │
│ │
│ Value: •••••••••••••••••••••••• │
│ │
│ This value will not appear in the conversation. │
│ │
│ [Enter] provide [Esc] decline │
╰────────────────────────────────────────────────────────────╯
Declining is a first-class outcome, not an error. The user is entitled to answer "you should not have asked — I will handle this myself":
[1] I will not share it — I will handle this myself
[2] This task does not need a secret
[3] I will use another approach (free text)
The model receives { "provided": false, "reason": "..." } and continues without it. The tool description should forbid re-asking after a decline, or the feature degrades into nagging.
Every step leaves a line in the feed, the way WebSearch does — audit by residue, at no extra cost:
● Secret received · GITHUB_TOKEN · 40 chars · fp a3f1c8e2 · session memory
● Secret used · GITHUB_TOKEN → bash (check.sh)
● Secrets wiped · 2 · session ended
Input requirements: echo disabled at the readline level; multi-line paste must work, since PEM keys and kubeconfigs are multi-line, so Enter inside a value must not submit; bracketed paste supported; Ctrl+C clears the buffer; the value is written to neither the prompt history nor the paste-cache.
Option 2 — adds session persistence
Option 1 plus an encrypted file under ~/.claude/session-secrets/<session-id>/ (mode 0600, AEAD, session key in the OS keychain, orphan sweep at startup) so secrets survive --resume.
One correctness note, since it is easy to get wrong: the guarantee comes from destroying the key, not from overwriting the file. On APFS, journaled ext4, any copy-on-write filesystem, or an SSD with wear leveling, overwriting in place does not destroy the original blocks — which is why man shred warns about journaling filesystems. Overwriting is hygiene; crypto-shredding is the guarantee.
Option 3 — minimum
Masked input passed to a single command, no storage between calls. Cheapest to build, still covers the most common case.
---
Security considerations
Non-goals, stated plainly — a feature that overpromises here would be worse than none:
- Not a secrets manager: nothing persists, nothing is managed, nothing is synced or shared.
- Does not protect against local malware running as the same user.
- Does not cover the return path: if a command prints the secret itself (
set -x,env,curl -v), it re-enters the context. That is the separate, already-filed problem (#80153, #88380, #71654). - Does not remove the need to rotate already-leaked secrets.
- Not for headless/CI;
-psessions should keep using environment variables. - Does not expand the model's authority. The value goes to the execution environment, not to the model. This is a change of channel, not a grant of new permissions — worth stating because adjacent requests (#88165) do ask for the broader capability of filling payment fields, and this proposal deliberately does not.
Risks worth naming:
- Social engineering. A tool the model uses to request secrets is also a phishing surface: a prompt injection could make the model open a trusted-looking panel. Mitigations: the value is never returned to the model under any outcome, so an injection learns only that a secret exists; the panel names the secret, its purpose and where it will be used; declining is one keystroke; every use is visible in the feed. The comparison is not against a perfect world but against today, where the same injection makes the model ask for the key in chat — and it leaks both to the attacker and into three on-disk stores. The feature strictly reduces the damage of that scenario rather than creating a new one.
- Behavioural side effect. Making the safe path easy may increase how often users hand over secrets at all. That trade is worth taking: those handovers are already happening, invisibly and permanently. Afterwards each one is explicit, visible in the feed, scoped to a session, and gone at exit.
- No new storage. Under Option 1 Claude Code stores nothing. The value lives as an environment variable of a child process, exactly as from a hand-typed
export. No new compliance surface. - Process environment is readable by the same user through
/proc/<pid>/environorps— this is CWE-214, and I am naming the residual weakness of my own proposal rather than leaving it for review to find. It is not a regression: it is true of anyexport, and strictly better than the status quo, where the credential goes on the command line and into the transcript. A stricter variant passes the value over a file descriptor.
Worth noting that the relevant guard already exists: after Microsoft Threat Intelligence showed the Read tool could reach /proc/self/environ in the GitHub Action, v2.1.128 hardened Read to unconditionally reject files under /proc/. Environment-based injection therefore builds on a boundary this product has already established, rather than requiring a new one.
- Memory is not absolute. Node.js does not guarantee erasure of strings, and swap or core dumps may capture the value. Hold it in a
Bufferand zero it explicitly. - Surface consistency. The tool is interactive by nature. IDE, web and Remote Control sessions should degrade with an explicit message rather than silently — note that Remote Control transcripts are stored server-side for cross-device sync, so the tool should be limited to local interactive sessions.
Alternative Solutions
op run,direnv,aws-vault, plain environment variables. All require setup ahead of time, and none of them exist at the moment of need, mid-session — which is exactly when the paste happens. This request neither replaces nor conflicts with them.- An MCP server. Cannot render a masked field in the TUI, and anything it collects still has to pass through the session to reach the model's tools.
- Hooks. I checked the hook reference rather than assuming.
PreToolUsecan rewrite a tool's arguments viaupdatedInput, so a hook can rewrite a command — but whatever it writes there becomes the tool input and is recorded, so the secret still lands in the transcript.PostToolUsehas noupdatedOutputcounterpart.SessionStartreturns onlyadditionalContext,initialUserMessage,sessionTitle,watchPathsandreloadSkills— nothing that sets environment variables. No hook event exposes any field that injects an environment variable into a Bash call. Several commenters on #29910 have built hook-based guards; the author of one of the most detailed replies there describes the approach as a hack he does not want, and notes that blocking every path by which an agent could read a secret is not realistic when it can run arbitrary shell commands. envinsettings.json. This is the one existing way to get a value into the Bash environment, and it is unsuitable for secrets on two counts: the value sits in a plaintext config file (CWE-312), and #71432 — closed as completed — established that these variables leak across trust boundaries into the Bash tool context in ways users do not expect. A per-session, in-memory channel is the opposite of a plaintext config entry.- Redacting secret-shaped strings. Helps the outbound path only, and cannot help before the value is typed. It is also the approach already tried on the
/feedbackupload path, which is why that path redacts and the transcript does not. - Instructions in
CLAUDE.md. Tried, industry-wide. #82796 is a report of the model violating exactly such an instruction; 64% non-revocation is the measured result of relying on user discipline.
Priority
Critical - Blocking my work
Feature Category
CLI commands and flags
Use Case Example
Today. Claude needs a GitHub token to check a workflow run and asks for it in chat. The user pastes a 40-character PAT. It is now in the transcript, in history.jsonl and in paste-cache/, and it is re-sent as context on every following turn. If the session later hits a bug and the user runs /bug, the token enters five-year retention and possibly a public issue. Per #78344, the user may then find the classifier blocking every way to actually use it. Per the industry number, the token is probably never rotated.
With this feature. Claude calls AskUserSecret({ name: "GITHUB_TOKEN", purpose: "call api.github.com on your behalf" }). A masked panel appears; the user pastes the token and sees only dots. The feed shows ● Secret received · GITHUB_TOKEN · 40 chars · fp a3f1c8e2 · session memory. Claude writes check.sh referencing $GITHUB_TOKEN, runs it with uses: ["GITHUB_TOKEN"], and reads the result. The transcript holds the script text and the variable name. The token exists only in one process's memory and is zeroed at exit. A later /bug carries nothing sensitive.
Additional Context
How these claims were verified
Every factual claim here is checkable, and I would rather state the method than ask for trust:
- The issue map — queried through the GitHub API on 2026-08-28: state,
stateReason, labels and creation dates for each issue cited. The distinction between "closed as duplicate", "closed as not planned" and "closed as completed" comes fromstateReason, not from reading titles. - The three on-disk locations — reproduced locally on Claude Code 2.1.247 (macOS) with the
greprecipe above, which anyone can run in under a minute. - Retention and storage figures — from the current Data usage and Security documentation, not from memory.
- Hook capabilities — read directly from the hooks reference rather than assumed; the field lists quoted under Alternatives are the documented ones.
- The existing masked prompt — confirmed from
claude mcp add --helpon the installed CLI. - Industry figures — quoted verbatim from GitGuardian's State of Secrets Sprawl 2026, including the Claude Code-specific leak rate.
- The
/prochardening — from Microsoft Security's published analysis of the Claude Code GitHub Action and Anthropic's v2.1.128 mitigation.
If any of these is wrong I would genuinely like to know, and will correct it.
Priority
High. 18 open issues over six months with nothing shipped, three of them filed in the last week; #71654 documents live credentials already burned; and #78344 shows users blocked from completing legitimate work after paying the leak cost.
Feature category
Interactive mode (TUI), with a security boundary.
---
Note on the reporting channel
This is a feature request about a missing mechanism, not a vulnerability report; nothing here describes an exploitable flaw in existing code, so per SECURITY.md it does not belong on HackerOne. I work in information security and am glad to help refine the threat model, review a design, or test an implementation.