Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
116 commits
Select commit Hold shift + click to select a range
cbfabe7
fix: redact MCP command env values and credential URLs in settings ba…
ibetitsmike Aug 26, 2026
7c2d5ad
fix: consume whole shell words in command redaction and resolve redac…
ibetitsmike Aug 26, 2026
543d699
fix: recognize env assignments after shell operators and fail closed …
ibetitsmike Aug 27, 2026
875b0c8
fix: collision-safe restore path keys, disguised-assignment detection…
ibetitsmike Aug 27, 2026
b9f9f51
fix: recognize bash append assignments in command redaction
ibetitsmike Aug 27, 2026
9fd948f
fix: detect assignments disguised by ANSI-C and locale quoting
ibetitsmike Aug 27, 2026
9404c60
fix: fail closed on expansion carriers and env split-string assignmen…
ibetitsmike Aug 27, 2026
38cdefb
fix: drop dead re-split whitespace rule, pin quote-embedded assignmen…
ibetitsmike Aug 27, 2026
082a530
fix: fail closed on interpreter script words and non-bash assignment …
ibetitsmike Aug 27, 2026
39b3c72
fix: treat legacy arithmetic as a carrier and scan published paths in…
ibetitsmike Aug 27, 2026
ebb3f32
fix: env operand names, brace-spliced assignments, and quote-split to…
ibetitsmike Aug 27, 2026
63528c4
fix: model braces as word content and accept arbitrary env operand as…
ibetitsmike Aug 27, 2026
63d16b5
fix: recognize all env option terminator spellings, scope quote-strip…
ibetitsmike Aug 27, 2026
904bd9d
fix: accept env split-string abbreviations, scan NUL-stripped content…
ibetitsmike Aug 27, 2026
0e6d4c7
fix: fail closed on expansion-brace groups in unconsumed command words
ibetitsmike Aug 27, 2026
a31f109
fix: normalize parsed command strings for the backstop, numeric env c…
ibetitsmike Aug 27, 2026
85cb5da
fix: scan expansion-stripped command strings so empty expansions cann…
ibetitsmike Aug 27, 2026
8844be2
fix: scope shell normalization to command strings so non-command fiel…
ibetitsmike Aug 27, 2026
9a40317
fix: linear sk- token check, bash-accurate quote removal, block conti…
ibetitsmike Aug 27, 2026
aee4b91
fix: fail closed on option values embedding assignments
ibetitsmike Aug 27, 2026
b6c24f2
fix: strip parameter expansions only in active quoting contexts
ibetitsmike Aug 27, 2026
1e2a553
fix: CRLF is not a continuation, example AWS key stays reviewable, at…
ibetitsmike Aug 27, 2026
6fa93ef
fix: bash-accurate word breaks, live special params, comments, url pe…
ibetitsmike Aug 27, 2026
418cbb7
fix: localize multi-member glob classes and state-dependent $!, comme…
ibetitsmike Aug 27, 2026
e3af002
fix: localize nondeterministic wildcards so a prefix glob cannot reco…
ibetitsmike Aug 27, 2026
230be73
fix: linear quote-aware glob analyzer, escaped class members localize…
ibetitsmike Aug 27, 2026
16537e5
fix: detect shell constructs only in active regions, keep repeated-ru…
ibetitsmike Aug 27, 2026
fe5fa07
fix: strip WHATWG tab and newline separators from urls before the cre…
ibetitsmike Aug 27, 2026
73aba1a
fix: nested brace expansion localizes, comment prose survives assignm…
ibetitsmike Aug 27, 2026
5301e2b
fix: positional and list expansions localize because the command can …
ibetitsmike Aug 27, 2026
3e02ac5
fix: always-set specials localize, oversized commands skip analysis, …
ibetitsmike Aug 27, 2026
baed571
fix: localize active named parameter expansions the command can populate
ibetitsmike Aug 27, 2026
2d8390a
fix: line continuations preserve comment boundaries in all three comm…
ibetitsmike Aug 27, 2026
24ba9e8
fix: normalize line continuations before shell analysis, localize ext…
ibetitsmike Aug 27, 2026
7653f8d
fix: localize eval reparse and assignment-free process substitutions
ibetitsmike Aug 27, 2026
c406b6b
fix: block GitLab issued tokens, localize write redirections
ibetitsmike Aug 27, 2026
65c613c
fix: cover all issued GitHub prefixes, localize pipelines and shell-s…
ibetitsmike Aug 27, 2026
c76f875
fix: cover remaining GitLab issued prefixes, localize shopt
ibetitsmike Aug 27, 2026
fde5856
fix: letter glob classes localize under inherited nocaseglob
ibetitsmike Aug 27, 2026
870fb7f
fix: localize source builtins, block xapp tokens, bound aggregate ana…
ibetitsmike Aug 27, 2026
8863eae
fix: named shell interpreters localize their reparsed script arguments
ibetitsmike Aug 27, 2026
b96a3bb
fix: normalize Windows interpreter spellings before the shell-name check
ibetitsmike Aug 27, 2026
275e640
fix: block Stripe live keys without an override
ibetitsmike Aug 27, 2026
e3b897f
fix: localize language-interpreter eval spellings, block npm tokens
ibetitsmike Aug 27, 2026
68b568d
fix: localize deferred traps and printf variable writes under inherit…
ibetitsmike Aug 27, 2026
b309d0f
fix: block temporary AWS keys and localize awk program operands
ibetitsmike Aug 27, 2026
88416fc
fix: recognize attached printf variable output options
ibetitsmike Aug 27, 2026
e5cc9b3
fix: localize MCP commands when Bash startup hooks are inherited
ibetitsmike Aug 27, 2026
60410d7
fix: localize sed program operands; publish commands despite ignored …
ibetitsmike Aug 27, 2026
34e4741
fix: localize mapfile callbacks and remote-reparse executables
ibetitsmike Aug 27, 2026
bd7c1b4
fix: recognize PHP -B/-E code options and local shell-reparse wrappers
ibetitsmike Aug 27, 2026
917f433
fix: bound interpreter lookup, recognize apiToken-style URL credentials
ibetitsmike Aug 27, 2026
1cd0b84
fix: reject over-limit MCP redaction queues before applying edits
ibetitsmike Aug 27, 2026
60337c2
fix: match x-prefixed URL credential names, end eval tracking at scri…
ibetitsmike Aug 27, 2026
253bb52
fix: batch jsonc edit application, keep eval tracking armed past opti…
ibetitsmike Aug 27, 2026
b1daf9b
fix: linear placeholder-run stripping, model read under inherited all…
ibetitsmike Aug 27, 2026
a8391cf
fix: localize cmd.exe command operands
ibetitsmike Aug 27, 2026
0553168
fix: preserve gap comments when batched deletion removes a final prop…
ibetitsmike Aug 27, 2026
fef5a8b
fix: cluster numeric interpreter switches before eval letters
ibetitsmike Aug 27, 2026
e39924f
fix: address round 52 review findings
ibetitsmike Aug 27, 2026
a68adb4
fix: harden backup module scanning and size checks
ibetitsmike Aug 27, 2026
7a85209
tests: tighten executable data URL coverage
ibetitsmike Aug 27, 2026
e2bb5eb
fix: address round 54 review findings
ibetitsmike Aug 27, 2026
ebbec98
fix: require a provider marker for credential-suffix URL parameters
ibetitsmike Aug 27, 2026
9f6ef33
fix: bind restore command approval to the exact planned MCP bytes
ibetitsmike Aug 27, 2026
376bf7c
fix: localize additional downstream evaluation modes
ibetitsmike Aug 27, 2026
9885531
fix: localize makefile launchers and tighten eval option parsing
ibetitsmike Aug 27, 2026
b37f78f
fix: localize runtime preloads and Git shell callbacks
ibetitsmike Aug 27, 2026
7ba8c30
fix: bind interpreter commands to published script inputs
ibetitsmike Aug 27, 2026
6bf665e
fix: inspect attached interpreter script-file options
ibetitsmike Aug 27, 2026
890c6ad
fix: cover APIM subscription keys and Tcl script launchers
ibetitsmike Aug 27, 2026
886dac6
fix: honor interpreter terminators and track Lua evaluation
ibetitsmike Aug 27, 2026
8097f7b
fix: localize PHP process-file script inputs
ibetitsmike Aug 27, 2026
d0fe82e
fix: localize JShell load-file inputs
ibetitsmike Aug 27, 2026
3fb8198
fix: localize Git config shell aliases
ibetitsmike Aug 27, 2026
f4f26de
fix: localize direct execution of published documents
ibetitsmike Aug 27, 2026
21434d5
fix: cover Swift, Elixir, and Git command config values
ibetitsmike Aug 27, 2026
7398417
fix: close env wrapper and encoded documentation gaps
ibetitsmike Aug 27, 2026
af572f4
fix: track Elixir and IEx evaluation operands
ibetitsmike Aug 27, 2026
792844e
fix: track Java source-file launcher mode
ibetitsmike Aug 27, 2026
bebb378
fix: scan issued-token shapes linearly and keep Slack placeholders re…
ibetitsmike Aug 28, 2026
b3af0c6
fix: restrict executable-name checks to command positions and track f…
ibetitsmike Aug 28, 2026
b61ab5a
fix: resolve published-document operands against the configured root
ibetitsmike Aug 28, 2026
94ebaee
fix: cover stdin-redirected documents, coproc/function bodies, and re…
ibetitsmike Aug 28, 2026
4721d66
fix: track systemd executor command operands
ibetitsmike Aug 28, 2026
6323869
fix: check CMake and CTest operands
ibetitsmike Aug 28, 2026
373125f
fix: reset parser state at separators, normalize operand paths, end d…
ibetitsmike Aug 28, 2026
431e16c
fix: localize Java argument files and sweep command-valued Git config…
ibetitsmike Aug 28, 2026
f3c88b0
fix: match the current user's named-home spelling of the settings root
ibetitsmike Aug 28, 2026
138ef24
fix: localize JShell startup files that name auto-published documents
ibetitsmike Aug 28, 2026
5e1586a
fix: localize hash -p remapping to published files and treat alias as…
ibetitsmike Aug 28, 2026
16763b5
chore: remove a stray review reply artifact from the repository root
ibetitsmike Aug 28, 2026
518a5f3
fix: localize every hash -p remap and the enable builtin loader
ibetitsmike Aug 28, 2026
21b23b8
fix: keep the script boundary after interpreter -- and track setarch …
ibetitsmike Aug 28, 2026
664b844
fix: track Erlang erl -eval, -run, and -s evaluation options
ibetitsmike Aug 28, 2026
d579b58
fix: localize cd into the collected root, gc.recentObjectsHook, and c…
ibetitsmike Aug 28, 2026
5e06d58
fix: gate stdin localization on executable input, resolve relative cd…
ibetitsmike Aug 28, 2026
8bb9fec
fix: localize !-valued submodule updates and bare names on a publishe…
ibetitsmike Aug 28, 2026
19c9cdd
fix: account for ambient cwd and loader execution in backup commands
ibetitsmike Aug 28, 2026
8cfe2e0
fix: localize npm launchers under inherited preloads and interactive …
ibetitsmike Aug 28, 2026
733a118
fix: account for env cwd, PHPRC, and Git executable search paths
ibetitsmike Aug 28, 2026
d5cc1ae
fix: track PATH scripts, runtime hooks, and wrapper working directories
ibetitsmike Aug 28, 2026
a1ff70a
fix: track mise command execution boundaries
ibetitsmike Aug 28, 2026
909a744
fix: resolve executable operands from shell cwd and track sqlite init…
ibetitsmike Aug 28, 2026
2dad64e
fix: localize Lua launchers under an inherited published LUA_INIT file
ibetitsmike Aug 28, 2026
4e44d82
fix: canonicalize inherited PATH entries and localize under dynamic-l…
ibetitsmike Aug 28, 2026
c66a998
fix: resolve loader search paths, inherited PYTHONPATH/CLASSPATH arch…
ibetitsmike Aug 28, 2026
8aaafbd
fix: canonicalize inherited env paths, track boot class paths and sql…
ibetitsmike Aug 28, 2026
225b3db
fix: localize direct java boot-class-path archives
ibetitsmike Aug 28, 2026
97f6154
fix: localize under inherited git config overrides and openssl config…
ibetitsmike Aug 28, 2026
ee5108a
fix: localize git launchers under inherited execution hooks
ibetitsmike Aug 28, 2026
8774b65
fix: track deprecated Git parameters and LLDB command files
ibetitsmike Aug 28, 2026
9f57e37
fix: track Perl debugger, uv run, GDB commands, and Ninja builds
ibetitsmike Aug 28, 2026
ad20e8e
fix: expand inherited JVM argument-file coverage
ibetitsmike Aug 28, 2026
f68116c
fix: track direct runtime loaders and inherited build hooks
ibetitsmike Aug 28, 2026
e6352db
fix: cover symlinked operands and inherited runtime hooks
ibetitsmike Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions src/browser/features/Settings/Sections/BackupSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -359,15 +359,23 @@ export function BackupSection() {
setStatusMessage(null);
setPreview(null);
setOverrideSecretScan(false);
// Cleared before the await, so a rejection (transport failure, not a scan result)
// cannot leave a previous scan's override rendering beside an unrelated error.
setSecretScanBlocked(false);
setSecretScanApproval(null);

try {
const result = await api.backup.preview(savedDraft);
if (!result.success) {
setActionError(getOperationErrorMessage(result.error));
// The scan state must describe this failure, not a previous push's: a stale
// digest would keep rendering an override the backend now rejects.
const blocked = result.error.code === "SECRET_DETECTED";
setSecretScanBlocked(blocked);
setSecretScanApproval(blocked ? (result.error.secretApproval ?? null) : null);
Comment thread
ibetitsmike marked this conversation as resolved.
return;
}
setPreview(result.data);
setSecretScanBlocked(false);
const nextApprovals = result.data.commandApprovals;
// An approval only covers the exact command text the user read, so a changed list
// has to be read again.
Expand Down Expand Up @@ -600,10 +608,10 @@ export function BackupSection() {
))}
</ul>
<p className="text-foreground text-xs font-medium">
Provider key files and dedicated secret files have no export path. MCP commands and URLs
are included verbatim; credential-like URL components require review, while literal MCP
header values are redacted. Inside skills and memory, only documentation is published
automatically; any other file waits for you to review it.
Provider key files and dedicated secret files have no export path. Env-style values in MCP
commands, URLs carrying credentials, and literal MCP header values are redacted;
publishing a command still requires review. Inside skills and memory, only documentation
is published automatically; any other file waits for you to review it.
</p>
</section>

Expand Down Expand Up @@ -713,7 +721,9 @@ export function BackupSection() {
)}
</section>

{secretScanBlocked ? (
{/* A credential-format block carries no approval digest and cannot be overridden,
so a dead override control must not suggest otherwise. */}
{secretScanBlocked && secretScanApproval !== null ? (
Comment thread
ibetitsmike marked this conversation as resolved.
<div className="border-border-light rounded-md border p-3">
<label className="flex items-start gap-2">
<Checkbox
Expand Down
4 changes: 4 additions & 0 deletions src/common/config/schemas/appConfigOnDisk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,17 @@ describe("AppConfigOnDiskSchema", () => {
"ssh+git:user:hunter2@",
"https://example.com/repo.git?access_token=hunter2",
"https://example.com/repo.git?passphrase=hunter2",
"https://example.com/repo.git?Ocp-Apim-Subscription-Key=hunter2",
"https://example.com/repo.git#access_token=hunter2",
]) {
expect(SettingsBackupSchema.safeParse({ ...base, repoUrl }).success).toBe(false);
}
for (const repoUrl of [
"https://github.com/me/dotfiles.git",
"https://github.com/me/dotfiles.git?client_id=mux",
// A descriptive option that happens to end in a credential word is not a
// provider-qualified signed-URL parameter.
"https://github.com/me/dotfiles.git?verify_signature=false",
"https://github.com/me/dotfiles.git?code=review&key=branch&session=docs",
"https://github.com/me/dotfiles.git#section=backup",
"ssh://git@example.com/repo.git",
Expand Down
29 changes: 28 additions & 1 deletion src/common/config/schemas/settingsBackup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,14 @@ export const CREDENTIAL_URL_PARAMETER_NAMES: ReadonlySet<string> = new Set([
"accesskeyid",
"accesstoken",
"apikey",
"apisecret",
"apitoken",
"appkey",
Comment thread
ibetitsmike marked this conversation as resolved.
"appsecret",
"apptoken",
"auth",
"authcode",
"authkey",
"authorization",
"authtoken",
"awsaccesskeyid",
Expand All @@ -90,6 +95,7 @@ export const CREDENTIAL_URL_PARAMETER_NAMES: ReadonlySet<string> = new Set([
"bearertoken",
"clientkey",
"clientsecret",
"clienttoken",
"consumersecret",
"credential",
"credentials",
Expand All @@ -100,25 +106,46 @@ export const CREDENTIAL_URL_PARAMETER_NAMES: ReadonlySet<string> = new Set([
"passwd",
"password",
"privatekey",
"privatetoken",
"pwd",
"refreshtoken",
"secret",
"secretaccesskey",
"secretkey",
"secrettoken",
"securitytoken",
"sessionid",
"sessiontoken",
"subscriptionkey",
"ocpapimsubscriptionkey",
"signature",
"token",
"xamzcredential",
"xamzsignature",
]);

/**
* Signed-URL families qualify the credential word with a header-style provider
* prefix (`X-Goog-Signature`, `X-Amz-Credential`, `x-oss-security-token`), so an
* `x`-led name ending in one of these unambiguous words matches without enumerating
* providers. Descriptive options that merely end in the word
* (`verify_signature=false`) carry no provider marker and stay accepted.
*/
const PROVIDER_CREDENTIAL_NAME =
/^x[a-z0-9]*(?:accesskeyid|credential|secretaccesskey|securitytoken|signature)$/;

function parametersContainCredential(
parameters: URLSearchParams,
names: ReadonlySet<string>
): boolean {
for (const [name, value] of parameters) {
const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, "");
if (value !== "" && names.has(normalizedName)) return true;
if (value === "") continue;
if (names.has(normalizedName)) return true;
// Header-style spellings prefix the same names with `x` (`x-api-key`,
// `X-Auth-Token`), so one stripped leading `x` matches the whole class.
if (normalizedName.startsWith("x") && names.has(normalizedName.slice(1))) return true;
if (PROVIDER_CREDENTIAL_NAME.test(normalizedName)) return true;
Comment thread
ibetitsmike marked this conversation as resolved.
}
return false;
}
Expand Down
33 changes: 15 additions & 18 deletions src/node/services/backup/backupService.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe("BackupService against a real repository", () => {
);
});

it("blocks a push when a backed-up file contains a token, and proceeds once allowed", async () => {
it("blocks a push outright when a backed-up file contains a credential token", async () => {
await writeFixtureFile(
muxRoot,
"AGENTS.md",
Expand All @@ -165,34 +165,31 @@ describe("BackupService against a real repository", () => {

const blocked = await service.push(settings);
expect(blocked.success).toBe(false);
if (blocked.success) throw new Error("Expected the secret scan to block the push");
if (blocked.success) throw new Error("Expected the credential backstop to block the push");
expect(blocked.error.code).toBe("SECRET_DETECTED");
expect(blocked.error.files).toContain("AGENTS.md");
// No approval digest: a credential-format match has no user override.
expect(blocked.error.secretApproval ?? null).toBeNull();
expect(await runGit(["--git-dir", originPath, "rev-list", "--count", "--all"])).toBe("0");

const allowed = await service.push(settings, {
approvedSecretDigest: blocked.error.secretApproval ?? undefined,
});
expect(allowed.success).toBe(true);
const stillBlocked = await service.push(settings, { approvedSecretDigest: "any-digest" });
expect(stillBlocked.success).toBe(false);
expect(await runGit(["--git-dir", originPath, "rev-list", "--count", "--all"])).toBe("0");
});

it("gates a low-entropy MCP URL credential until the exact payload is approved", async () => {
it("redacts a low-entropy MCP URL credential instead of gating the push", async () => {
const url = "https://user:hunter2@example.com/mcp?api_key=abc123";
await writeFixtureFile(muxRoot, "mcp.jsonc", JSON.stringify({ servers: { private: { url } } }));

const blocked = await service.push(settings);
expect(blocked.success).toBe(false);
if (blocked.success) throw new Error("Expected the URL credential gate to block the push");
expect(blocked.error.code).toBe("SECRET_DETECTED");
expect(blocked.error.files).toEqual(["mcp.jsonc"]);
expect(await runGit(["--git-dir", originPath, "rev-list", "--count", "--all"])).toBe("0");
const pushed = await service.push(settings);
expect(pushed.success).toBe(true);
if (!pushed.success) throw new Error("Expected the redacted payload to push cleanly");
expect(pushed.data.redactions).toEqual(["servers.private.url"]);

const allowed = await service.push(settings, {
approvedSecretDigest: blocked.error.secretApproval ?? undefined,
});
expect(allowed.success).toBe(true);
const clone = await cloneOrigin("url-credential-verify");
expect(await fs.readFile(path.join(clone, "mux/mcp.jsonc"), "utf-8")).toContain(url);
const published = await fs.readFile(path.join(clone, "mux/mcp.jsonc"), "utf-8");
expect(published).not.toContain("hunter2");
expect(published).toContain(REDACTED_BACKUP_VALUE);
});

it("requires exact-payload approval before publishing an MCP command", async () => {
Expand Down
Loading
Loading