feat: resolve {{secret:NAME}} and {{script:NAME}} placeholders in the MCP server - #2
Open
steffen-heil-secforge wants to merge 1 commit into
Open
steffen-heil-secforge wants to merge 1 commit into
steffen-heil-secforge wants to merge 1 commit into
Conversation
steffen-heil-secforge
changed the base branch from
base/upstream-main
to
main
September 13, 2026 15:04
steffen-heil-secforge
force-pushed
the
feature/secret-placeholders
branch
4 times, most recently
from
September 13, 2026 15:40
502fa6d to
c6f19ba
Compare
steffen-heil-secforge
force-pushed
the
feature/secret-placeholders
branch
2 times, most recently
from
September 13, 2026 17:06
50c174b to
a0035d2
Compare
… MCP server
Lets a password be filled into a page without ever passing the plaintext
to a tool, so it never enters the conversation transcript. fill,
fill_form, type_text and evaluate_script substitute {{secret:NAME}} with
the contents of <data-dir>/secrets/NAME in this MCP server, immediately
before the input is dispatched to the browser.
A secret is consumed (deleted) once the call succeeds; ":keep" retains it
and ":raw" preserves a trailing newline.
{{script:NAME}} does the same for <data-dir>/scripts/NAME, so the same
JavaScript can be run repeatedly without the caller writing it out in
every call. The code is still sent to the browser each time; only the
caller is spared repeating it. A script takes no modifiers: it is never
consumed and always used exactly as stored.
Resolution runs in exactly two passes, scripts then secrets, so a script
may carry a secret but substituted content is never rescanned: scripts do
not nest and a secret value is never interpreted as a placeholder.
Both directories live under this server's own per-OS data directory,
keyed off "chrome-devtools-mcp" rather than any MCP client, and can be
relocated with CHROME_DEVTOOLS_MCP_SECRETS_DIR and
CHROME_DEVTOOLS_MCP_SCRIPTS_DIR. The per-OS resolution telemetry already
used moves to utils/paths.ts so both share it.
References are basename-only so they cannot escape their directory, and
resolved values are kept out of every response: type_text echoes the
unresolved text, and fill errors report the placeholder rather than the
substituted value.
Adds a secret-handling skill covering staging by reference, login and
2FA flows, script reuse, and the request-body caveat.
steffen-heil-secforge
force-pushed
the
feature/secret-placeholders
branch
from
September 13, 2026 17:14
a0035d2 to
641c6c2
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.
Problem
fill,fill_formandtype_texttake the text to type as a parameter. Filling a password therefore writes the plaintext into the tool call — and tool calls are recorded in the conversation transcript. The secret is then durably logged, and any later reader of the transcript has it.Approach
The model never needs to know the secret — only which secret goes in which field. This adds substitution inside the MCP server: the value is spliced in immediately before the input is dispatched to the browser, so the plaintext never has to be passed to a tool.
resolves from
<data-dir>/secrets/github-login-7f3a. Secrets are staged there by reference (pass show x > <data-dir>/secrets/x), never by writing the literal.{{secret:NAME}}{{secret:X}}{{secret:X:raw}}{{secret:X:keep}}{{secret:X:raw:keep}}Consume-on-use is the default so secrets do not linger. Deletion happens only after the call succeeds, so a failed fill leaves the secret staged for a retry. Modifiers parse in any order; an unknown one is an error rather than being silently ignored.
{{script:NAME}}Reads
<data-dir>/scripts/NAME, so the same JavaScript can be run repeatedly without the caller writing it out in every call. The code is still sent to the browser each time — only the caller is spared repeating it. A script takes no modifiers: it is meant to be reused, so it is never consumed, and it is code, so it is used exactly as stored (never trimmed). Passing:raw,:keepor anything else is a clear error.Two passes, no stacking
Resolution runs in exactly two passes — scripts, then secrets — so a stored script may carry
{{secret:...}}and still get it resolved in the same call. Substituted content is never rescanned, which means:The second property matters: a secret's contents are influenced by whatever wrote the file, so re-scanning them would let file content drive further file reads.
Keeping values out of responses
Substitution is pointless if the value comes straight back out, so three echo paths were closed:
type_texttypes the resolved value but echoes the unresolved text.fillFormElementandselectOptionreport the placeholder in error messages via a separatedisplayValue, instead of the substituted value.Directories
Both live under this server's own per-OS data directory, keyed off
chrome-devtools-mcprather than any MCP client, so they are the same whichever client launched the server:~/Library/Application Support/chrome-devtools-mcp%LOCALAPPDATA%\chrome-devtools-mcp\Data$XDG_DATA_HOME/chrome-devtools-mcpRelocatable with
CHROME_DEVTOOLS_MCP_SECRETS_DIR/CHROME_DEVTOOLS_MCP_SCRIPTS_DIR(which is also how to put secrets on tmpfs, e.g.$XDG_RUNTIME_DIR/..., without Linux-only code here).The per-OS resolution that
telemetry/persistence.tsalready had moves toutils/paths.tsso both share one copy instead of two.References are basename-only —
/,..and absolute paths are rejected — so a reference cannot escape its directory.Scope / limitations
Testing
22 unit tests in
tests/secrets.test.ts: substitution,:raw,:keep,:raw:keep, repeated and mixed references, traversal rejection, unknown modifiers, missing-file errors, consumption, script substitution and modifier rejection, and all three two-pass properties (secret-from-script resolves; nested script does not; secret value not reinterpreted). These need no browser and pass.The telemetry suite passes unchanged, confirming the
getDataFolderextraction is behaviour-preserving.The fill-into-a-page integration path is not covered by a new test — it requires a live browser.
Skill
Adds
skills/secret-handling/SKILL.md, per the feature checklist in CONTRIBUTING.md. It covers staging a secret by reference (and whyecho "<literal>"defeats the mechanism), the placeholder table, login and 2FA flows, script reuse, the two-pass rule, and a warning not to re-read the login request body afterwards.🤖 Generated with Claude Code