Add option to disable 'Command contains quoted characters in flag names' warning

Open 💬 26 comments Opened Feb 23, 2026 by AndersHeie

Problem

After a recent Claude Code update, every bash command containing quoted characters in flag values triggers a confirmation prompt with the warning:

Command contains quoted characters in flag names

This happens on completely normal commands like git commit -m "message" or bun run build --flag "value". In previous versions, these commands executed without prompts (assuming they matched the allowlist).

Impact

  • Breaks existing workflows — commands that previously matched Bash(git commit *) or Bash(bun run *) allowlist patterns now require manual yes/no confirmation every time.
  • Especially disruptive for agentic use — multi-agent orchestration workflows that delegate to subagents are constantly interrupted by these prompts.
  • False positive rate is very high — legitimate, safe commands are flagged far more often than actual injection attempts.

Request

Please add one or more of the following:

  1. A setting to disable this specific check — e.g., "disableQuotedCharWarning": true in settings.json
  2. Respect the existing allowlist — if a command matches an allow pattern, skip the quoted-character check
  3. Documentation — explain exactly what patterns trigger this warning so users can reformulate commands to avoid it

Environment

  • Claude Code (latest, Feb 2026)
  • WSL2 / Linux
  • Using project-level settings.json with allowlist + denylist permissions

Current workaround

The only workaround is adding Bash(*) to the allowlist, which defeats the purpose of having granular allow patterns.

View original on GitHub ↗

26 Comments

cboos · 4 months ago

Claude Opus 4.6 uses echo "---" all the time to separate command outputs (e.g., file .../aa && echo "---" && file .../ab), so now we get this warning and block all the time as well :-(

stonematt · 4 months ago

"Command contains quoted characters in flag names" fires too broadly

Problem

The safety check that warns about quoted characters in Bash commands triggers on harmless literal strings, requiring manual approval for commands like:

git log --oneline -5 && echo "---" && git status --short

The prompt says "Command contains quoted characters in flag names" — but "---" is a literal string passed to echo, not a flag name. There's no injection risk.

Impact

This creates unnecessary approval interruptions during normal agentic flow. Users end up adding blanket CLAUDE.md rules to work around it ("never use quotes"), which is a worse outcome than a well-scoped check.

Expected behavior

The check should distinguish between:

  • Quoted characters in flag/argument positions (e.g., --flag="$(rm -rf /)") — worth flagging
  • Quoted literal strings passed to safe commands like echo — should not trigger

Reproduction

Run any Bash command with a quoted literal, e.g.:

echo "---"
echo "hello world"
printf "%s\n" "done"

All trigger the approval prompt unnecessarily.

m-atoms · 4 months ago

Given how widely Claude Code is used and how disruptive this is to the default workflow, I'm surprised there isn't more activity in here. Is there an easy fix I'm missing? I added CLAUDE.md rules in the meantime:

 - Bash commands: never chain with `&&` — use separate parallel Bash calls instead. Claude Code's compound-command 
security check ([#16561](https://github.com/anthropics/claude-code/issues/16561)) prompts even when each component 
is individually allowed. Also avoid quoted strings resembling flags (e.g., `echo "---"`) which trigger "quoted characters 
in flag names" warnings ([#27957](https://github.com/anthropics/claude-code/issues/27957)).
mrballcb · 4 months ago

Using beans is also affected by this. While working, beans continually updates its markdown with things like:

beans update helm-chart-hnlf --body-replace-old "- [ ] Update all container resource specifications to support resourceSizing" --body-replace-new "- [✅ ] Update all container resource specifications to support resourceSizing"

I can no longer tab away from anything working with agents because it prompts for every single step across each agent. I'm not willing to --dangerously-skip-permissions for some things.

Stanzilla · 4 months ago

It fails on things like this for me all the time:

<img width="1570" height="604" alt="Image" src="https://github.com/user-attachments/assets/004e0302-c002-40a2-8aa2-2c6b2ddb2e36" />

jonas-debeuk · 4 months ago

also getting blocked a lot by this - even dangerously sku permissions flag didn't seem to help 🤔

DeanBaron · 4 months ago

also getting blocked a lot by this. I run CLIs that accepts JSON arguments, this makes CC always request permissions which slows down work significantly.

outbound · 4 months ago

Same. Glad not the only one.

dpchamps · 4 months ago

I've observed that the changes in some of the permission prompts called out here and in other issues basically demands that you use claude code with dangerously-skip-permissions in order to have an effective session that does not require you to babysit the process.

The introduction of such fine grained permissions may have the inverse effect on what the team is trying to achieve from a security perspective, essentially inducing "alarm fatigue". Meaning, the prompts are so noisy and frequent that people with either dangerously-skip-permissions, or they'll blindly hit approve.

It would be nice to have some clarity on what the suggested usage is, and whether or not the introduction of these gates was intentional or is a bug.

lukeojones · 4 months ago

Jumping on this bandwagon, this has had a massive impact on coding workflows. It's too noisy now. Official config or guidance would really help.

jonas-klesen · 4 months ago

This really needs a proper fix ASAP!

KK-DMKI · 4 months ago

It also blocks the sub-agents, because they sometimes are stuck in confirmations. It's a P1 bug and needs to be resolved.

dmki · 4 months ago

Anthropic, please fix this bug. Don't make mistakes. Ultrathink.

1

btoro · 4 months ago

This is my working solution for the above. I setup a pretooluse hook that nudges CC to behave. I used it to solve this issue here and similar quirks that started popping up, like compound command use. Anytime something new pops up, I just update the shell script with some instructions to get around it.

Setup in ~/.claude/settings.json:

  {
    "hooks": {
      "PreToolUse": [
        {
          "matcher": "Bash",
          "hooks": [
            {
              "type": "command",
              "command": "~/.claude/hooks/no-compound.sh"
            }
          ]
        }
      ]
    }
  }

no-compound.sh

#!/usr/bin/env bash
set -euo pipefail

cmd=$(jq -r '.tool_input.command // ""')

# --- Block Zsh-only syntax ---
# =() is Zsh process substitution and does not work in bash
if echo "$cmd" | grep -Eq '=\(' ; then
  cat >&2 <<'EOF'
BLOCKED: =() is Zsh process substitution and is not supported. Use bash-compatible alternatives:
  - For temp file from command output: use a variable or write to a temp file with mktemp
  - For diff-style comparisons: use <() which is bash process substitution
  - For assignments with arrays: use arr=( ) with a space after =
Do not use Zsh-specific syntax. Rewrite using bash.
EOF
  exit 2
fi

# --- Block git -C <path> ---
if echo "$cmd" | grep -Eq '^\s*git\s+-C\s'; then
  cat >&2 <<'EOF'
BLOCKED: Do not use `git -C <path>`. Always run git directly from the working directory.
If you need to operate in a different directory, cd there first in a separate command.
EOF
  exit 2
fi

# --- Block git commit --amend ---
if echo "$cmd" | grep -Eq '^\s*git\s+commit\s.*--amend'; then
  cat >&2 <<'EOF'
BLOCKED: Do not use `git commit --amend`. Always create a new commit instead.
Amending can destroy previous commit contents, especially after a failed pre-commit hook.
EOF
  exit 2
fi

# --- Block compound command operators: &&, ||, ; ---
stripped=$(echo "$cmd" | sed -e "s/'[^']*'//g" -e 's/"[^"]*"//g')
if echo "$stripped" | grep -Eq '(\s&&\s|\s\|\|\s|;\s*\S)'; then
  cat >&2 <<'EOF'
BLOCKED: Compound commands are not allowed. Do not chain commands with &&, ||, or ;
Run each command as a separate Bash tool call so that:
  - Each command's exit code is visible
  - Errors are caught immediately
  - Output is not interleaved
Break this into individual commands and run them one at a time.
EOF
  exit 2
fi

# --- Warn on pipe usage ---
if echo "$cmd" | grep -Eq '[^|]\|[^|]'; then
  cat >&2 <<'EOF'
HINT: Avoid piping (|) commands when possible. Prefer dedicated tools:
  - Use Read instead of cat/head/tail
  - Use Grep instead of grep/rg
  - Use Glob instead of find/ls
Only use pipes when there is genuinely no alternative (e.g., passing input to a CLI tool).
EOF
fi

exit 0
Collapse
Cleroth · 4 months ago

This is very irritating (to the point where even using codex is less painful than getting prompted every few minutes).
Getting blocked on commands like ls and find is crazy.
Needs fixing.

TyreReviews · 4 months ago

Jumping in, pretty frustrating.

alokdhir · 4 months ago

+1

benchi · 4 months ago

+1 - this pretty much breaks any agentic development workflow that doesn't run with --dangerously-skip-permissions. As a result, I'm now following the lead of some other folks to move my development into a sandboxed devcontainer, where I'm willing to run with --dangerously-skip-permissions.

snailwei · 3 months ago

wait for fix...

qremmax · 3 months ago

+1 waiting for this fix, perhaps it could still warning, but not stopping the whole process on every 30 / 60 seconds.

yurukusa · 3 months ago

Hook workaround until this is fixed upstream:
You can auto-approve commands that trigger the quoted-characters warning with a PreToolUse hook:

INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty')
[ "$TOOL" != "Bash" ] && exit 0
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if echo "$CMD" | grep -qE '^(git\s+commit|git\s+tag|npm\s+run|bun\s+run|yarn|pnpm|make|cargo|go\s+build|python|node)'; then
  echo '{"decision":"approve"}'
  exit 0
fi
exit 0

Register in ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/approve-quoted-flags.sh" }]
      }
    ]
  }
}

This auto-approves common dev commands (git commit, npm run, bun run, etc.) regardless of quoted flag values, while still prompting for unfamiliar commands.
For a broader safety setup with 8 hooks that covers this and other common permission annoyances:

npx cc-safe-setup
david-crazyamber · 3 months ago

I've been testing a more automated workaround based on @yurukusa's and @btoro's Hook idea. Instead of hardcoding commands in a shell script, I've developed a Node.js hook that dynamically reads your ~/.claude/settings.json and auto-approves any command that already exists in your permissions.allow list.

This solves the "Alarm Fatigue" without sacrificing security, as it only bypasses the quoted-character warning for commands you've already explicitly trusted in your config.

How it works:

  1. It reads the Bash(xxx:*) rules directly from your existing settings.json.
  2. It converts those rules into Regex patterns.
  3. If the incoming command matches your allowlist, it returns {"decision":"approve"} to bypass the manual prompt.
  4. It includes a logging feature for easy debugging.

Setup:

  1. Create ~/.claude/hooks/approve-logic.js (and run chmod +x on it):
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

// 日志文件路径
const LOG_FILE = path.join(process.env.HOME, '.claude/hooks/debug.log');

function log(message) {
  const timestamp = new Date().toISOString();
  fs.appendFileSync(LOG_FILE, `[${timestamp}] ${message}\n`);
}

async function main() {
  try {
    // 1. 读取标准输入
    const inputData = fs.readFileSync(0, 'utf8');
    if (!inputData) return;
    
    const { tool_name, tool_input } = JSON.parse(inputData);
    if (tool_name !== 'Bash' || !tool_input.command) return;

    const currentCmd = tool_input.command;
    const settingsPath = path.join(process.env.HOME, '.claude/settings.json');
    
    log(`--- New Check ---`);
    log(`Command: ${currentCmd}`);

    if (!fs.existsSync(settingsPath)) {
      log(`Error: Settings file not found at ${settingsPath}`);
      return;
    }

    // 2. 解析 settings.json
    const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
    const allowList = settings.permissions?.allow || [];

    // 3. 构造正则
    const patterns = allowList
      .filter(item => item.startsWith('Bash('))
      .map(item => {
        // 提取括号内容并去掉末尾的 :*
        let raw = item.replace(/^Bash\((.*)\)$/, '$1').replace(/:\*$/, '');
        // 转义正则特殊字符,处理星号
        const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*');
        return {
          original: item,
          regex: new RegExp(`^${escaped}`)
        };
      });

    // 4. 匹配逻辑
    let matchedRule = null;
    const isApproved = patterns.some(p => {
      if (p.regex.test(currentCmd)) {
        matchedRule = p.original;
        return true;
      }
      return false;
    });

    if (isApproved) {
      log(`MATCHED: [${matchedRule}] -> Auto-approving.`);
      console.log(JSON.stringify({ decision: "approve" }));
    } else {
      log(`NO MATCH: Command did not match any allowlist patterns.`);
    }

  } catch (e) {
    log(`CRITICAL ERROR: ${e.message}`);
    process.exit(0);
  }
}

main();
  1. Register the hook in your ~/.claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node ~/.claude/hooks/approve-logic.js"
          }
        ]
      }
    ]
  }
}

This effectively makes the security check respect the user-defined allowlist (SSOT), which I believe is how it should behave by default. Works great in my local tests

snailwei · 3 months ago

This is very annoying, our work is hitting Yes all day long!!!

<img width="614" height="166" alt="Image" src="https://github.com/user-attachments/assets/e6ae5e6e-b0f3-4644-bc15-79be6d860e6b" />

falkgeist · 3 months ago

+1

reaver · 3 months ago

Here is @david-crazyamber's solution that also captures project and local permission rules.

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');

const HOME = process.env.HOME || process.env.USERPROFILE;
const LOG_FILE = path.join(HOME, '.claude/hooks/debug.log');

function log(message) {
  const timestamp = new Date().toISOString();
  fs.appendFileSync(LOG_FILE, `[${timestamp}] ${message}\n`);
}

function readJsonSafe(filePath) {
  try {
    if (fs.existsSync(filePath)) {
      return JSON.parse(fs.readFileSync(filePath, 'utf8'));
    }
  } catch (e) {
    log(`Warning: Could not parse ${filePath}: ${e.message}`);
  }
  return null;
}

function collectAllowLists() {
  const allowRules = [];

  // Global settings: ~/.claude/settings.json
  const globalSettings = readJsonSafe(path.join(HOME, '.claude/settings.json'));
  if (globalSettings?.permissions?.allow) {
    allowRules.push(...globalSettings.permissions.allow);
  }

  // Global local settings: ~/.claude/settings.local.json
  const globalLocal = readJsonSafe(path.join(HOME, '.claude/settings.local.json'));
  if (globalLocal?.permissions?.allow) {
    allowRules.push(...globalLocal.permissions.allow);
  }

  // Project-level settings: .claude/settings.json and .claude/settings.local.json in CWD
  const cwd = process.cwd();
  const projectSettings = readJsonSafe(path.join(cwd, '.claude/settings.json'));
  if (projectSettings?.permissions?.allow) {
    allowRules.push(...projectSettings.permissions.allow);
  }
  const projectLocal = readJsonSafe(path.join(cwd, '.claude/settings.local.json'));
  if (projectLocal?.permissions?.allow) {
    allowRules.push(...projectLocal.permissions.allow);
  }

  return allowRules;
}

async function main() {
  try {
    const inputData = fs.readFileSync(0, 'utf8');
    if (!inputData) return;

    const { tool_name, tool_input } = JSON.parse(inputData);
    if (tool_name !== 'Bash' || !tool_input.command) return;

    const currentCmd = tool_input.command;

    log(`--- New Check ---`);
    log(`Command: ${currentCmd}`);

    const allowList = collectAllowLists();
    log(`Allow rules found: ${JSON.stringify(allowList)}`);

    const patterns = allowList
      .filter(item => item.startsWith('Bash('))
      .map(item => {
        let raw = item.replace(/^Bash\((.*)\)$/, '$1').replace(/:\*$/, '');
        const escaped = raw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*');
        return {
          original: item,
          regex: new RegExp(`^${escaped}`)
        };
      });

    let matchedRule = null;
    const isApproved = patterns.some(p => {
      if (p.regex.test(currentCmd)) {
        matchedRule = p.original;
        return true;
      }
      return false;
    });

    if (isApproved) {
      log(`MATCHED: [${matchedRule}] -> Auto-approving.`);
      console.log(JSON.stringify({ decision: "approve" }));
    } else {
      log(`NO MATCH: Command did not match any allowlist patterns.`);
    }

  } catch (e) {
    log(`CRITICAL ERROR: ${e.message}`);
    process.exit(0);
  }
}

main();
yurukusa · 3 months ago

/tmp/gh-comment-27957.md