Skip to content

feat: resolve {{secret:NAME}} and {{script:NAME}} placeholders in the MCP server - #2

Open
steffen-heil-secforge wants to merge 1 commit into
mainfrom
feature/secret-placeholders
Open

steffen-heil-secforge wants to merge 1 commit into
mainfrom
feature/secret-placeholders

Conversation

@steffen-heil-secforge

@steffen-heil-secforge steffen-heil-secforge commented Sep 13, 2026

Copy link
Copy Markdown
Member

Problem

fill, fill_form and type_text take 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.

fill_form({ uid: "...", value: "{{secret:github-login-7f3a}}" })

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}}

Placeholder Trailing newline File after use
{{secret:X}} stripped deleted
{{secret:X:raw}} kept deleted
{{secret:X:keep}} stripped kept
{{secret:X:raw:keep}} kept kept

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, :keep or 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:

  • a script cannot pull in another script, and
  • a secret's value is never interpreted as a placeholder.

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_text types the resolved value but echoes the unresolved text.
  • fillFormElement and selectOption report the placeholder in error messages via a separate displayValue, instead of the substituted value.

Directories

Both live under this server's own per-OS data directory, keyed off chrome-devtools-mcp rather than any MCP client, so they are the same whichever client launched the server:

  • macOS ~/Library/Application Support/chrome-devtools-mcp
  • Windows %LOCALAPPDATA%\chrome-devtools-mcp\Data
  • otherwise $XDG_DATA_HOME/chrome-devtools-mcp

Relocatable 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.ts already had moves to utils/paths.ts so 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

  • Input-side only. The plaintext still appears in the login request body, so it is visible to someone who deliberately inspects it with the network tools afterwards. Output redaction was considered and deliberately left out: pages do not normally echo passwords back, and substring scrubbing is defeated by any encoding the page applies, so it would be defence-in-depth rather than a guarantee.
  • Names must be unique. The secrets directory is shared by all MCP servers running in parallel, so a generic name can be clobbered by another session or deleted while still needed. The parameter descriptions tell the model this, with the reason.
  • Directory permissions are not enforced.

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 getDataFolder extraction 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 why echo "<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

@steffen-heil-secforge
steffen-heil-secforge changed the base branch from base/upstream-main to main September 13, 2026 15:04
@steffen-heil-secforge
steffen-heil-secforge force-pushed the feature/secret-placeholders branch 4 times, most recently from 502fa6d to c6f19ba Compare September 13, 2026 15:40
@steffen-heil-secforge steffen-heil-secforge changed the title feat: fill secrets from ~/.claude/mcp-secrets without putting them in the transcript feat: resolve {{secret:NAME}} and {{script:NAME}} placeholders server-side Sep 13, 2026
@steffen-heil-secforge steffen-heil-secforge changed the title feat: resolve {{secret:NAME}} and {{script:NAME}} placeholders server-side feat: resolve {{secret:NAME}} and {{script:NAME}} placeholders in the MCP server Sep 13, 2026
@steffen-heil-secforge
steffen-heil-secforge force-pushed the feature/secret-placeholders branch 2 times, most recently from 50c174b to a0035d2 Compare September 13, 2026 17:06
… 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.
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