Skip to content

setup defaults --agent claude silently skips Claude Code: JSON comment stripper corrupts // inside permission strings #219

Open

Description

@brandonvham

Summary

firecrawl setup defaults --agent claude reports

! Skipped Claude Code settings because settings.json is not valid JSON

against a settings.json that is valid JSON. The cause is the regex comment-stripper in configureClaudeDefaults, which deletes from any // to end-of-line — including // that occurs inside a JSON string literal.

This is not a rare edge case on Windows. Claude Code writes local-path permission rules in the form Read(//c/Users/...), so the leading // appears in the file as soon as the user approves any path-scoped read or edit. One such rule is enough to make the whole file unparseable to the CLI, permanently.

Environment

  • firecrawl-cli 1.23.3 (via npx -y firecrawl-cli@1)
  • Windows 11, Node v24.13.0

Reproduction

Two settings.json files, both valid JSON, differing by one line.

Case A — ~/.claude/settings.json:

{
  "permissions": {
    "allow": [
      "Read(//c/Users/someone/projects/**)"
    ]
  },
  "model": "opus"
}

Case B — identical, with that rule replaced by "Bash(git status)".

Run firecrawl setup defaults --agent claude against each:

result
A ! Skipped Claude Code settings because settings.json is not valid JSON — no deny written
B ✓ Disabled Claude Code native WebSearch/WebFetch — deny written correctly

JSON.parse() accepts both files as-is.

Root cause

removeJsonComments() in src/utils/web-defaults.ts (observed in dist/utils/web-defaults.js:28):

function removeJsonComments(content) {
    return content
        .replace(/\/\*[\s\S]*?\*\//g, '')
        .replace(/(^|[^:\\])\/\/.*$/gm, '$1');
}

The [^:\\] guard exempts :// (protecting URLs) and escaped slashes, but the // in "Read(//c/Users/...)" is preceded by (. The line is truncated to "Read(, leaving an unterminated string, and the subsequent JSON.parse in configureClaudeDefaults throws:

Bad control character in string literal in JSON at position 988

The catch then reports the file as invalid and skips the agent.

Because the stripper runs over the entire file before parsing, a single // anywhere poisons the parse for every key.

Two related problems in the same function

1. The comment support is self-defeating. The write path is:

await writeText(filePath, `${JSON.stringify(nextConfig, null, 2)}\n`);

The file is rebuilt from the parsed object, so any comments the stripper removed in order to parse are silently discarded on write — along with the user's key order and formatting. The stripper exists to tolerate comments, and the writer destroys them.

2. The success line prints unconditionally. configureWebDefaults can skip every agent and the command still ends with:

Firecrawl is now the default web provider for supported AI agents.

and exit code 0. That is what masked the failure initially — the warning scrolls past and the closing line reads as success. It also makes the failure invisible in CI.

Suggested fixes

Minimal — try strict JSON first

Valid JSON never reaches the regex. Verified working against Case A above:

  if (existing && existing.trim()) {
      try {
-         config = JSON.parse(removeJsonComments(existing));
+         try {
+             config = JSON.parse(existing);
+         }
+         catch {
+             config = JSON.parse(removeJsonComments(existing));
+         }
      }
      catch {
          return { /* skipped */ };
      }
  }

Proper — use a string-aware JSONC parser

Regex stripping cannot be made correct; [^:\\] was already a patch for https://, and (// is the next hole. Any // inside a string value will keep reproducing this. jsonc-parser (used by VS Code for this exact problem) handles both the read and the write:

import { parse, modify, applyEdits } from 'jsonc-parser';

const config = parse(existing) ?? {};
// ...compute nextDeny...
const edits = modify(existing, ['permissions', 'deny'], nextDeny, {
  formattingOptions: { insertSpaces: true, tabSize: 2 },
});
await writeText(filePath, applyEdits(existing, edits));

modify + applyEdits also resolves problem 1 — surgical edits preserve comments, key order, and formatting instead of rewriting the document.

Reporting

Gate the closing message on results.some(r => !r.skipped), and return a non-zero exit code when an agent was skipped, so the failure is visible to users and to CI.

Workaround

Add the entries by hand to ~/.claude/settings.json:

"permissions": {
  "deny": ["WebSearch", "WebFetch"]
}

Note that setup defaults --undo fails the same way, so undoing also has to be done by hand.

Activity

  1. johnsnow92 commented on Sep 22, 2026

    @johnsnow92

    The /* … */ half of the same removeJsonComments() regex fails in a worse way than the // case above. It doesn't make settings.json unparseable. It produces valid JSON with different content, so setup defaults writes it back, reports success, and silently removes a deny rule.

    Environment

    • firecrawl-cli 1.24.3 (current npm latest)
    • macOS, Node v26.8.1

    Reproduction

    A temp HOME keeps real settings untouched:

    T=$(mktemp -d); mkdir -p "$T/.claude"
    printf '%s\n' '{ "permissions": { "deny": ["Read(**/*.key)", "Read(**/secrets/**)"] } }' > "$T/.claude/settings.json"
    HOME="$T" firecrawl setup defaults --agent claude -y
    jq -c .permissions.deny "$T/.claude/settings.json"

    Expected

    ["Read(**/*.key)","Read(**/secrets/**)","WebSearch","WebFetch"]

    Actual: the command prints ✓ Disabled Claude Code native WebSearch/WebFetch, and the file contains:

    ["Read(**secrets/**)","WebSearch","WebFetch"]

    The lazy /\/\*[\s\S]*?\*\//g matches from the /* in **/*.key to the */ in **/secrets/. That deletes the text between them and merges two deny rules into one pattern that protects neither path as written. Because the result still parses, nothing flags it.

    --undo runs the same stripper. It removes WebSearch/WebFetch but cannot restore the lost rule. Afterwards the file holds only ["Read(**secrets/**)"], so undoing doesn't repair the damage.

    Why it matters

    Globs like **/*.key, **/*.pem and **/secrets/** are the standard way to deny reads of credentials in Claude Code. Any file with a /* in one string and a */ later in any string, even in another section, is affected. The failure is silent and weakens protections the user configured deliberately.

    Suggested fix

    1. Try strict JSON.parse first and strip comments only if that fails; Claude Code writes strict JSON. (A string-aware scanner, as in fix(web-defaults): safely parse JSON with strings containing slashes … #247, also works.)
    2. Before writing, check that the rewrite changed nothing except permissions.deny, and refuse to write otherwise.
    3. Add a regression test for this case, in the style of fix(web-defaults): safely parse JSON with strings containing slashes … #247's tests:
    it('does not treat /* inside permission globs as a block comment', async () => {
      await write('.claude/settings.json', JSON.stringify({
        permissions: { deny: ['Read(**/*.key)', 'Read(**/secrets/**)'] },
      }));
      await configureWebDefaults({ agents: ['Claude Code'] });
      const saved = JSON.parse(await read('.claude/settings.json'));
      expect(saved.permissions.deny).toEqual([
        'Read(**/*.key)', 'Read(**/secrets/**)', 'WebSearch', 'WebFetch',
      ]);
    });

    Workaround: add "WebSearch" and "WebFetch" to permissions.deny by hand. setup defaults --agent codex is unaffected, because it edits one TOML line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions