Repository navigation
feat(secret-prompt): ask the release manager for an upload token without exposing it - #1696
Merged
Merged
Conversation
Merged
1 task done
…out exposing it Release steps such as the PyPI upload of convenience packages need a credential only the release manager holds. When an agent runs the step, or a script with no terminal, the usual "read -s" prompt has nowhere to go, so the token ends up in a file or on a command line, and from there in shell history, ps or the agent's transcript. secret-prompt asks for the secret in the terminal when there is one, and otherwise as a macOS osascript dialog, a zenity/kdialog dialog on a Linux desktop, or pinentry. It then execs one command with the secret in one environment variable. Every prompt names that command, so the release manager can see where the secret goes before pasting it; a --prefix check rejects a wrong paste, and a cancelled prompt runs nothing. A backend that cannot ask falls through to the next one. The release-build template, the promote skill's convenience-artefacts reference and the release-management spec now recommend declaring credentialed stage/publish commands wrapped in it. Generated-by: Claude Code (Opus 5)
…e secret Showing the command is what lets the release manager check where the token goes, so it must not be possible to make that line lie. The command is now resolved to an absolute path before asking, shown in full (or refused, never truncated) and run exactly as shown; the prompt also lists every --set variable and any inherited variable sharing the secret's prefix, such as a TWINE_REPOSITORY_URL. Names that change how a command runs (PATH, HOME, LD_*, DYLD_*, PYTHON*, ...) cannot be set, --prompt and --title must be one plain line, osascript gets "--" before the text, and zenity/kdialog decline text they might render as markup. Generated-by: Claude Code (Opus 5)
… of the prompt The "It will be passed to" and "with:" lines are what the operator checks before pasting, so nothing the caller controls may rewrite them: an escape sequence, newline or bidi character in a command argument, a value or an inherited variable name could still fake those lines. And the prompt text reaches a dialog in its argv, so inherited values that may be credentials (another token, a URL with userinfo or a query) must not be copied into it. NODE_AUTH_TOKEN, npm's publish credential, was blocked by the NODE_ denial although it only authenticates. Generated-by: Claude Code (Opus 5)
Hiding every inherited value whose name did not look like routing also hid variables that redirect the secret (a TWINE_CERT, an XSP_TOKEN_URL), so the prompt could not show where it went. Only values whose names say they are a credential are hidden now; a URL always shows its host. A value that can be neither shown nor hidden safely, such as a URL carrying a user and password or a credential-named variable that also changes how the command runs, stops the prompt instead of being quietly hidden. Generated-by: Claude Code (Opus 5)
… the secret Showing every inherited value whose name did not look like a credential put any credential under an unexpected name (TOKEN2, a bare token@host) into the dialog's argv. Hiding by default fails closed, and a URL still always shows its host, so where the secret may go stays visible either way. Generated-by: Claude Code (Opus 5)
- main() ended in `try: os.execve(...)` with the only return in its `except`, so the end of the function looked reachable and returned None (py/mixed-returns). execve replaces the process or raises; the 127 is now returned after the try, so every path returns explicitly. - The tests imported secret_prompt both as a module and with `from ... import` (py/import-and-import-from). They import the names they use, and patch module attributes by their dotted path. Generated-by: Claude Opus 5
potiuk
force-pushed
the
feat/secret-prompt
branch
from
October 18, 2026 18:21
2240eea to
d2ccc51
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What we are asking for
Please review and accept:
tools/secret-prompt/(substrate:release, stdlib-only Python). It asks the release manager for a secret and execs one command with it in one environment variable:osascriptdialog,zenity/kdialogon a Linux desktop, orpinentry.stage_command/publish_commandentries wrapped in it. This goes in therelease-build.mdtemplate, the promote skill'sconvenience-artefacts.mdand the release-management spec. The skills still render those commands verbatim and never ask for or handle the token themselves.We are not asking to change how any skill behaves, to add a dependency, or to store a credential anywhere.
Why
Release steps like the PyPI upload need a credential only the release manager holds. When an agent runs the step on their behalf, or a script has no terminal,
read -shas nowhere to prompt. The token then ends up in a file or on a command line, and from there in shell history,psor the agent's transcript. This came up during Apache Airflow's 2026-10 providers waves: three PyPI uploads (two final, one rc) ran from an agent session using exactly thisosascripthidden-answer dialog, with the token never written down.Guarantees
exec, so that command keeps its exit status.--prefixarrives; cancelling runs nothing.--setvariable plus any inherited variable sharing the secret's prefix (TWINE_*forTWINE_PASSWORD, so aTWINE_REPOSITORY_URLpointing elsewhere is visible). An agent picks the command, so this is what stops a look-alike prompt from handing the token to something that prints or forwards it.--prompt/--titlemust be one plain line.zenity/kdialogdecline text they might render as markup.*_URL,*_HOST,*_REPOSITORY,*_INDEX,*_USERNAME,*_CERT, …) and has nothing credential-like in it. Every other value is listed asNAME=(set; value not shown), since a dialog receives the prompt in its argv. That way a credential under an unexpected name stays hidden. A URL always shows its host, so where the secret may go is never hidden: under a routing name up to its path, otherwise asscheme://host/***.--setvalues are shown as given.user:pw@proxy,alice:s3cret,host/simple?token=…), a value too quoted to parse, and a credential-named variable that also changes how the command runs (an*_ASKPASS). The error names the variable to unset.--env/--setrefuse names that change how the command runs (PATH,HOME,LD_*,DYLD_*,PYTHON*,NODE_*exceptNODE_AUTH_TOKEN,GIT_*, …).LD_PRELOADor a~/.pypircset up beforehand. The README says so.Testing
70 tests run without a real dialog. They cover:
--;pinentry;ruff and mypy are clean, and the full
workspace-pytesthook passed on the first commit.Cross-references are refreshed: the capability map row, plus the regenerated
docs/vendor-neutrality.mdand.github/labeler.yml. Therelease-management-lifecyclespec is updated with a new behaviour bullet.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Opus 5)