fix(daemon): keep spaced settings JSON whole when reading a Claude command line (#2665)

The Claude command-line reader keeps a --settings or --mcp-config JSON value whole when it's written with spaces, by tracking brace depth on those two options only, so a seat launched through a proxy alias with spaced settings can be verified and its conversation read (#2508). Other options' values aren't grouped. Contributed by Nikhi00718, with billbenetti's diagnosis.
This commit is contained in:
NIKHIL authored and GitHub committed 2026-11-08 09:29:26 -08:00
1 parent 4d7c50d514
commit b40bc71ca6
2 files changed
+37 -5

No files matched your search

@@ -25,26 +25,37 @@ export type NativeRuntime = "claude-code" | "codex";
function tokens(command: string): string[] {
// ps flattens argv: inline settings JSON retains its string delimiters.
// A quote inside a name or filename is literal, not a shell span delimiter.
// A `--settings` or `--mcp-config` JSON value keeps the spaces between its members too, as in
// `{"env": {"A": "b"}}`: it stays one token until its braces balance. Braces inside its strings don't count, and
// a brace that opens any other option's value is literal (#2508).
const result: string[] = [];
let start = 0;
let quote: string | null = null;
let depth = 0;
const append = (end: number) => {
if (start === end) return;
const token = command.slice(start, end);
result.push(token[0] === '"' && token.at(-1) === '"'
? token.slice(1, -1) : token);
};
const structured = () => command.startsWith('"{', start) || command[start] === "{"
|| command.startsWith("'{", start) || command.startsWith("--settings={", start)
|| command.startsWith('--settings="', start) || command.startsWith("--mcp-config={", start)
|| command.startsWith('--mcp-config="', start);
const jsonValue = () => command.startsWith("--settings={", start) || command.startsWith("--mcp-config={", start)
|| (command[start] === "{" && (result.at(-1) === "--settings" || result.at(-1) === "--mcp-config"));
for (let index = 0; index < command.length; index += 1) {
const char = command[index]!;
if (quote !== null) {
if (quote === '"' && char === "\\") { index += 1; continue; }
if (char === quote) quote = null;
} else if (char === '"' && (command.startsWith('"{', start) || command[start] === "{"
|| command.startsWith("'{", start) || command.startsWith("--settings={", start)
|| command.startsWith('--settings="', start) || command.startsWith("--mcp-config={", start)
|| command.startsWith('--mcp-config="', start))) {
} else if (char === '"' && structured()) {
quote = char;
} else if (/\s/.test(char)) {
} else if (char === "{" && jsonValue()) {
depth += 1;
} else if (char === "}" && depth > 0) {
depth -= 1;
} else if (/\s/.test(char) && depth === 0) {
append(index);
start = index + 1;
}
@@ -483,6 +483,8 @@ describe("Claude identity with inline settings in ps output", () => {
permissions: { allow: ["Bash(npm run test:*)"] },
hooks: { PreToolUse: [{ hooks: [{ type: "command", command: 'node "/fixture with spaces/check.cjs" --label "it\'s fine"', timeout: 5 }] }] },
})],
// #2508: a proxy that writes its settings with spaces between JSON members, as ps shows the argument.
["spaced JSON", '{"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8080", "ANTHROPIC_AUTH_TOKEN": "fixture"}}'],
];
function input(args: string) {
const list = [
@@ -513,6 +515,9 @@ describe("Claude identity with inline settings in ps output", () => {
`--settings /fixture/settings.json --session-id ${token}`,
`--settings /fixture/review's-settings.json --session-id ${token}`,
`--settings /fixture/review"settings.json --session-id ${token}`,
// #2508: a brace inside a spaced value's string doesn't end the value.
`--settings {"env": {"note": "a } b {"}} --resume ${token}`,
`--settings={"env": {"A": "b"}} --session-id=${token}`,
])("accepts inline and file settings: %s", async args => {
expect((await verifyClaudePaneProcess(input(args)))?.process.pid).toBe(21);
expect((await observeClaudeDelivery(input(args))).state).toBe("verified");
@@ -594,12 +599,28 @@ describe("Claude identity with inline settings in ps output", () => {
it.each([
`--settings ${JSON.stringify({ label: `two words --session-id ${token}` })}`,
`--settings {"env":{"note":"text --session-id ${token} --model trailing`,
// #2508: spaced settings stay one value, so an identity inside them, or after a value that never closes, proves nothing.
`--settings {"env": {"note": "text --session-id ${token}"}}`,
`--settings {"env": {"note": "x"} --session-id ${token}`,
`--settings ${settings[0]![1]} --session-id ${token} --session-id different`,
`--settings ${settings[1]![1]} --session-id ${token} --unknown`,
])("does not promote ambiguous argv: %s", async args => {
expect(await verifyClaudePaneProcess(input(args))).toBeNull();
expect((await observeClaudeDelivery(input(args))).state).not.toBe("verified");
});
// #2508: braces group only a `--settings` or `--mcp-config` value. Any other value that opens with `{` still ends at
// a space, so the options after it are read as on main.
it("keeps a second resume after another option's brace-led value visible", async () => {
const actual = input(`--resume ${token} --name {label --resume ${parent}`);
expect(await verifyClaudePaneProcess(actual)).toBeNull();
expect((await observeClaudeDelivery(actual)).state).not.toBe("verified");
});
it("reads the identity after another option's brace-led value", async () => {
const actual = input(`--name {label --resume ${token}`);
expect((await verifyClaudePaneProcess(actual))?.process.pid).toBe(21);
expect((await observeClaudeDelivery(actual)).state).toBe("verified");
expect(await verifyClaudePaneProcess({ ...actual, expectedToken: "different" })).toBeNull();
});
it.each([
`--resume ${parent} --fork-session --session-id ${token}`,
`--fork-session --session-id=${token} --resume=${parent}`,