[MODEL] Should not encourage shell command substitution `$(...)` in system prompt — causes permission approval dialog spam

Status Open
Maintainer reply None cached
Activity 6 comments · opened Mar 6, 2026

Preflight Checklist

  • [x] I have searched existing issues for similar behavior reports
  • [x] This report does NOT contain sensitive information (API keys, passwords, etc.)

Type of Behavior Issue

Claude's behavior changed between sessions

What You Asked Claude to Do

Recent versions of Claude Code increasingly use shell command substitution ($(...))
in generated commands — apparently encouraged by the system prompt itself.
However, any command containing $(...) always triggers a manual permission
approval dialog
, regardless of allow rules defined in settings.json.
This results in approval dialog spam for otherwise-allowed commands.

Background

Claude Code's permission system correctly treats shell command substitution as
requiring explicit approval (reasonable from a security perspective).
However, the system prompt appears to encourage patterns like:

git commit --message "$(cat << 'EOF'
...
EOF
)"

This means even commands covered by allow rules (e.g. Bash(git commit:*))
always prompt for manual approval, defeating the purpose of the allowlist entirely.

What Claude Actually Did

I have added Bash(git commit:*) to the allow list in ~/.claude/settings.json

  1. Let Claude Code to perform a git commit
  2. Claude generates: git commit -m "$(cat << 'EOF' ...)"
  3. Permission approval dialog appears every time, despite the allowlist entry

Expected Behavior

Claude Code should use allowlist-compatible command forms, e.g.:

git commit -m "1st line" -m "2nd line"

This would respect the Bash(git commit:*) allow rule and avoid unnecessary prompts.

Files Affected

Permission Mode

Accept Edits was ON (auto-accepting changes)

Can You Reproduce This?

Yes, every time with the same prompt

Steps to Reproduce

As written in the What Claude Actually Did section

Claude Model

Opus

Relevant Conversation

Impact

Low - Minor inconvenience

Claude Code Version

v2.1.70

Platform

Anthropic API

Additional Context

When asked to stop using this pattern, Claude Code itself acknowledged that shell command substitution ($(...)) is directed by its system prompt.

Adding an explicit instruction to CLAUDE.md to avoid shell command substitution
partially mitigates the issue, but it requires strong phrasing and is not reliable —
the system prompt appears to override user instructions in CLAUDE.md.

The proper fix should be in the system prompt itself.

View original on GitHub ↗

6 Comments

aronstrandberg · 5 months ago

This is very annoying! Would be super neat with a bundled Git tool to make this smoother.

in-op · 5 months ago

Agreed. It's basically impossible to let it go off and explore a repository nowadays without having to babysit all the shell commands it wants to run.

philipmuir · 5 months ago

Agreed. This is so annoying that I am actively searching how to get Claude code (Opus) to stop using $()

yurukusa · 5 months ago

This happens because there are two separate enforcement layers, and they don't talk to each other:

  1. Permission allowlist — pattern-matches against the command string. Bash(git commit:*) works here.
  2. Safety heuristics — separate code path that scans for shell metacharacters ($(), backticks, newlines in compound commands). These fire after the allowlist check passes, and there's no setting to suppress them.

The system prompt explicitly tells the model to use the heredoc $(cat << 'EOF'...) pattern for commit messages. So the model is doing exactly what it's told — and then the safety heuristic blocks it. The model's own instructions conflict with the safety layer.

Workaround that actually works: Add this to your project's CLAUDE.md:

When committing, pass the commit message directly with -m flag.
Do NOT use heredoc or $() syntax for commit messages.
For multi-line messages, use multiple -m flags.

This isn't 100% reliable (CLAUDE.md is guidance, not enforcement), but it reduces heredoc commits significantly. If you need it enforced, a PreToolUse:Bash hook can intercept and reject commands containing $( before they hit the safety heuristic:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "if echo \"$CLAUDE_TOOL_INPUT\" | jq -r '.command' | grep -q '\\$(' ; then echo 'Use -m flag instead of $() for commits' >&2; exit 2; fi"
      }]
    }]
  }
}

Exit code 2 = hard block. The model will retry with a simpler command form.

Running 1,000+ hours of autonomous Claude Code sessions, I've found that any rule you need reliably enforced should be in a hook, not just in CLAUDE.md or settings. The permission/heuristic system is pattern-matching text; hooks are executing code.

snailwei · 5 months ago

This is very annoying!

I keep hitting Yes all day long!!!

<img width="346" height="168" alt="Image" src="https://github.com/user-attachments/assets/0600e97b-1491-43d6-b23a-ede3cf863032" />

southp · 2 months ago

This can also happen when backticks are used for formatting messages rather than actual command substitutions:

<img width="1904" height="752" alt="Image" src="https://github.com/user-attachments/assets/9d7acc1c-f88f-4b43-b074-bc8d9ecda1b8" />