From 35524954cb771613a6205951ed04d516270c1102 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Sat, 2 May 2026 00:37:28 -0700 Subject: [PATCH] fix(export): use '#' as sed delimiter so SECRET_REGEX '|' alternation parses Reproduces with: any staged file matching the secret regex (e.g. a test fixture containing an OpenRouter / Anthropic / OpenAI key shape). The redact step ran: sed -i -E "s|${SECRET_REGEX}|${REDACT_PLACEHOLDER}|g" "$f" SECRET_REGEX is itself a `|`-separated alternation of provider patterns (`(sk-or-v1-...)|(sk-ant-api...)|(...)`). After bash expansion, sed sees multiple fields where it expected three, and bails with: sed: -e expression #1, char 67: unknown option to `s' The export then fails on the VM ("sprite exec failed (exit 1)") even though the user already approved the redaction in the host prompt. Fix: switch the sed delimiter to '#'. None of the regex tokens (provider prefixes, char classes, quantifiers, PEM marker) contain '#', and neither does the placeholder, so the substitution is unambiguous. '|' inside the pattern is now correctly interpreted by sed -E as alternation, which is what we wanted all along. Adds a regression test asserting the script uses 's#...#...#g' and not 's|...|...|g'. Bumps CLI 1.0.35 -> 1.0.36. --- packages/cli/package.json | 2 +- packages/cli/src/__tests__/export.test.ts | 12 ++++++++++++ packages/cli/src/commands/export.ts | 6 +++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 25cd4c825..d7b76af09 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.0.35", + "version": "1.0.36", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/export.test.ts b/packages/cli/src/__tests__/export.test.ts index 6d99e1a26..6a4510655 100644 --- a/packages/cli/src/__tests__/export.test.ts +++ b/packages/cli/src/__tests__/export.test.ts @@ -217,6 +217,18 @@ describe("buildExportScript", () => { expect(s).not.toContain("Possible secrets detected in staged files; aborting"); }); + it("uses '#' as the sed delimiter — '|' would clash with SECRET_REGEX alternation", () => { + // Regression: the sed substitution previously used '|' as its delimiter + // ("s|${SECRET_REGEX}|${REDACT}|g"). Because SECRET_REGEX itself contains + // '|' (it's a |-separated alternation of provider patterns), bash + // expansion produced a string sed parsed with the wrong number of fields, + // failing with "unknown option to `s'". '#' is absent from both the regex + // and the placeholder, so the substitution is unambiguous. + const s = buildExportScript(opts); + expect(s).toContain('sed -i -E "s#${SECRET_REGEX}#${REDACT_PLACEHOLDER}#g"'); + expect(s).not.toContain('sed -i -E "s|${SECRET_REGEX}|${REDACT_PLACEHOLDER}|g"'); + }); + it("pauses before commit with needs_confirmation when ALLOW_REDACT=0 (first pass)", () => { const s = buildExportScript({ ...opts, diff --git a/packages/cli/src/commands/export.ts b/packages/cli/src/commands/export.ts index 60e73ccc7..76ef6f55c 100644 --- a/packages/cli/src/commands/export.ts +++ b/packages/cli/src/commands/export.ts @@ -345,7 +345,11 @@ export function buildExportScript(opts: { ' printf "%s\\n" "$SECRET_HITS" >&2', " while IFS= read -r f; do", ' [ -z "$f" ] && continue', - ' sed -i -E "s|${SECRET_REGEX}|${REDACT_PLACEHOLDER}|g" "$f"', + " # Delimiter is '#' — SECRET_REGEX contains '|' (alternation), so '|'", + " # as the sed delimiter would close the pattern at the first alternative", + " # (\"unknown option to s\"). '#' appears in neither the regex nor the", + " # placeholder, so the substitution is unambiguous.", + ' sed -i -E "s#${SECRET_REGEX}#${REDACT_PLACEHOLDER}#g" "$f"', ' done <<< "$SECRET_HITS"', " # Re-stage so the redacted blobs replace the originals in the index.", " git add -A",