[BUG] Permission prompt incorrectly triggers on cd instead of the actual command in compound bash statements

Open 💬 47 comments Opened Feb 24, 2026 by prezzz

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When Claude Code generates a compound bash command using && (e.g.: cd /some/path && git add file && git commit -m "msg"), the permission approval prompt incorrectly identifies cd as the action requiring approval, showing a cd:* permission request instead of prompting for the meaningful actions (git add, git commit).
The problem repeats on every execution. The cd:* prompt keeps appearing and cannot be whitelisted for the session, making the workflow unusable.

What Should Happen?

In earlier versions, the same compound command correctly prompted for git add and git commit as the actions requiring permission, and allowed the user to whitelist them for the session.

Error Messages/Logs

Steps to Reproduce

  1. Open Claude Code in a project
  2. Ask Claude to stage and commit a specific file (e.g. CLAUDE.md)
  3. Claude generates a command like: cd /path/to/repo && git add CLAUDE.md && git commit -m "update"
  4. Observe the permission prompt . It asks for approval of cd:* instead of git add / git commit
  5. Approve it. The prompt appears again on the next execution, it cannot be whitelisted

Claude Model

Opus

Is this a regression?

Yes, this worked in a previous version

Last Working Version

Around 2.1.40 it was working fine

Claude Code Version

2.1.52

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

Windows Terminal

Additional Information

_No response_

View original on GitHub ↗

47 Comments

fschwiet · 4 months ago

I'm seeing this too, in my case the the /some/path in "cd /some/path" is the directory I launched claude from so it shouldn't even be necessary.

EDIT: I'm also on Windows.

kosinal · 4 months ago

I have the similar issue:

<img width="1089" height="437" alt="Image" src="https://github.com/user-attachments/assets/8701e20d-36ef-4aeb-a4f2-4f35403fb5bc" />

In all commands, the Claude code tries to cd to current directory.

fschwiet · 4 months ago

My guess is a path comparison is happening and failing due to different normalized forms being compared, comparing /c/code/project and c:\code\project, for instance. I noticed that when I tell Claude to stop using "cd " first with the project directory it will use the -C parameter with git (git -C <project_directory).

kosinal · 4 months ago

@fschwiet : Thank your, I will try this. I tried to adjust instructions to use cd only when needed, but it does not work.

Or maybe this will finally convince me to buy Mac :)

fschwiet · 4 months ago

@kosinal I'm finding the instructions added to claude.md aren't helping that much, every session we go through the same process where I have to tell it to stop doing a "cd <project dir> &&" then tell it stop doing the "git -C <projectDir>" to prevent excessive permission checks.

kosinal · 4 months ago

@fschwiet interesting, where do you put it instead? Directly into prompt, or into skill/agent?

fschwiet · 4 months ago

It's in my CLAUDE.md and when that is ignored I deny permission requests that are changing paths and tell it not to do that. It still needs to be reminded every session, so I don't have a good fix.

gtbuchanan · 4 months ago

I'm having some success with the following _very_ explicit workaround in CLAUDE.md:

## Shell

- NEVER use `cd` to change to the current working directory before running a command.
  The working directory is already set — just run the command directly.
  IMPORTANT: On Windows with Git Bash, `/c/Users/...` and `C:\Users\...` are the SAME path.
  Do NOT do `cd /c/Users/.../project && command` if the cwd is
  `C:\Users\...\project`. They are equivalent — skip the `cd`.
edgariscoding · 4 months ago

Same issue here

enricoros · 4 months ago

I put CAPITALIZED instructions in CLUDE.md as well as stored in the memory files, but at best I get a 50% reduction.

Anyone wants to bisect a version that works?

azlojutro · 4 months ago

I have been able to circumvent this by creating hooks that block command using this chaining and instructs it otherwise.

{
  "hooks": 
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/no-cd-chaining.sh\"
          },
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/no-git-dash-c.sh\""
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# Hook: Block chaining cd with other commands

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Match cd chained with other commands via && ; ||
if echo "$COMMAND" | grep -qP '^\s*cd\s+.+?\s*(&&|;|\|\|)\s*' || \
   echo "$COMMAND" | grep -qP '(&&|;|\|\|)\s*cd\s+'; then
  cat <<HOOKEOF >&2
BLOCKED: Do not chain cd with other commands.
Instead, do these as SEPARATE Bash tool calls:
1. Run pwd to check your current directory
2. Run cd to navigate to the directory you need
3. Run your actual command
Never combine cd with another command using && or ; or ||.
HOOKEOF
  exit 2
fi

exit 0

#!/bin/bash
# Hook: Block git -C usage, enforce cd then git instead

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Match git -C <path> (with or without quotes around path)
if echo "$COMMAND" | grep -qP '(^|\s)git\s+-C\s+'; then
  cat <<HOOKEOF >&2
BLOCKED: Do not use git -C <path>.
Instead, do these as SEPARATE Bash tool calls:
1. Run pwd to check your current directory
2. Run cd to navigate to the directory you need
3. Run your git command without -C
HOOKEOF
  exit 2
fi

exit 0

edgariscoding · 4 months ago
I have been able to circumvent this by creating hooks that block command using this chaining and instructs it otherwise. `` { "hooks": "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash .claude/hooks/no-cd-chaining.sh" }, { "type": "command", "command": "bash .claude/hooks/no-git-dash-c.sh" } ] } ] } } ` ` #!/bin/bash # Hook: Block chaining cd with other commands INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') # Match cd chained with other commands via && ; || if echo "$COMMAND" | grep -qP '^\s*cd\s+.+?\s*(&&|;|\|\|)\s*' || \ echo "$COMMAND" | grep -qP '(&&|;|\|\|)\s*cd\s+'; then cat <<HOOKEOF >&2 BLOCKED: Do not chain cd with other commands. Instead, do these as SEPARATE Bash tool calls: 1. Run pwd to check your current directory 2. Run cd to navigate to the directory you need 3. Run your actual command Never combine cd with another command using && or ; or ||. HOOKEOF exit 2 fi exit 0 ` ` #!/bin/bash # Hook: Block git -C usage, enforce cd then git instead INPUT=$(cat) COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') # Match git -C <path> (with or without quotes around path) if echo "$COMMAND" | grep -qP '(^|\s)git\s+-C\s+'; then cat <<HOOKEOF >&2 BLOCKED: Do not use git -C <path>. Instead, do these as SEPARATE Bash tool calls: 1. Run pwd to check your current directory 2. Run cd to navigate to the directory you need 3. Run your git command without -C HOOKEOF exit 2 fi exit 0 ``

I tried this but couldnt get it to work, at least not on git bash on Windows. It seems like Claude’s own security checks is triggered before any hooks run:

"Compound commands with cd and git require approval to prevent bare repository attacks"

lothbrokragnar620-hash · 4 months ago

Prompting on cd instead of git add or git commit breaks the whole approval model. Once the control point attaches to shell glue instead of the mutating step, you lose both signal and usable whitelisting. I'm working on Daedalab around that exact boundary: approve the risky action, not the wrapper. If you're curios: www.daedalab.app

azlojutro · 4 months ago
> I have been able to circumvent this by creating hooks that block command using this chaining and instructs it otherwise. > `` > { > "hooks": > "PreToolUse": [ > { > "matcher": "Bash", > "hooks": [ > { > "type": "command", > "command": "bash .claude/hooks/no-cd-chaining.sh" > }, > { > "type": "command", > "command": "bash .claude/hooks/no-git-dash-c.sh" > } > ] > } > ] > } > } > ` > > > > > > > > > > > > ` > #!/bin/bash > # Hook: Block chaining cd with other commands > > INPUT=$(cat) > COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') > > # Match cd chained with other commands via && ; || > if echo "$COMMAND" | grep -qP '^\s*cd\s+.+?\s*(&&|;|\|\|)\s*' || \ > echo "$COMMAND" | grep -qP '(&&|;|\|\|)\s*cd\s+'; then > cat <<HOOKEOF >&2 > BLOCKED: Do not chain cd with other commands. > Instead, do these as SEPARATE Bash tool calls: > 1. Run pwd to check your current directory > 2. Run cd to navigate to the directory you need > 3. Run your actual command > Never combine cd with another command using && or ; or ||. > HOOKEOF > exit 2 > fi > > exit 0 > ` > > > > > > > > > > > > ` > #!/bin/bash > # Hook: Block git -C usage, enforce cd then git instead > > INPUT=$(cat) > COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') > > # Match git -C <path> (with or without quotes around path) > if echo "$COMMAND" | grep -qP '(^|\s)git\s+-C\s+'; then > cat <<HOOKEOF >&2 > BLOCKED: Do not use git -C <path>. > Instead, do these as SEPARATE Bash tool calls: > 1. Run pwd to check your current directory > 2. Run cd to navigate to the directory you need > 3. Run your git command without -C > HOOKEOF > exit 2 > fi > > exit 0 > ` I tried this but couldnt get it to work, at least not on git bash` on Windows. It seems like Claude’s own security checks is triggered before any hooks run: "Compound commands with cd and git require approval to prevent bare repository attacks"

@edgariscoding I just checked my files and I have edited the comment to reflect what I have in the settings.json. You need to use $CLAUDE_PROJECT_DIR in the path and that works for me. If that doesn't work for you, open a session with Claude and get it to iterate until the path works, because this definitely works for me and has been really nice

troy-phoenix · 4 months ago

Sweet Odin's Raven, please fix this

edgariscoding · 4 months ago
> > I have been able to circumvent this by creating hooks that block command using this chaining and instructs it otherwise. > > `` > > { > > "hooks": > > "PreToolUse": [ > > { > > "matcher": "Bash", > > "hooks": [ > > { > > "type": "command", > > "command": "bash .claude/hooks/no-cd-chaining.sh" > > }, > > { > > "type": "command", > > "command": "bash .claude/hooks/no-git-dash-c.sh" > > } > > ] > > } > > ] > > } > > } > > ` > > > > > > > > > > > > > > > > > > > > > > > > ` > > #!/bin/bash > > # Hook: Block chaining cd with other commands > > > > INPUT=$(cat) > > COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') > > > > # Match cd chained with other commands via && ; || > > if echo "$COMMAND" | grep -qP '^\s*cd\s+.+?\s*(&&|;|\|\|)\s*' || \ > > echo "$COMMAND" | grep -qP '(&&|;|\|\|)\s*cd\s+'; then > > cat <<HOOKEOF >&2 > > BLOCKED: Do not chain cd with other commands. > > Instead, do these as SEPARATE Bash tool calls: > > 1. Run pwd to check your current directory > > 2. Run cd to navigate to the directory you need > > 3. Run your actual command > > Never combine cd with another command using && or ; or ||. > > HOOKEOF > > exit 2 > > fi > > > > exit 0 > > ` > > > > > > > > > > > > > > > > > > > > > > > > ` > > #!/bin/bash > > # Hook: Block git -C usage, enforce cd then git instead > > > > INPUT=$(cat) > > COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') > > > > # Match git -C <path> (with or without quotes around path) > > if echo "$COMMAND" | grep -qP '(^|\s)git\s+-C\s+'; then > > cat <<HOOKEOF >&2 > > BLOCKED: Do not use git -C <path>. > > Instead, do these as SEPARATE Bash tool calls: > > 1. Run pwd to check your current directory > > 2. Run cd to navigate to the directory you need > > 3. Run your git command without -C > > HOOKEOF > > exit 2 > > fi > > > > exit 0 > > ` > > > I tried this but couldnt get it to work, at least not on git bash on Windows. It seems like Claude’s own security checks is triggered before any hooks run: > "Compound commands with cd and git require approval to prevent bare repository attacks" [@edgariscoding](https://github.com/edgariscoding) I just checked my files and I have edited the comment to reflect what I have in the settings.json. You need to use $CLAUDE_PROJECT_DIR` in the path and that works for me. If that doesn't work for you, open a session with Claude and get it to iterate until the path works, because this definitely works for me and has been really nice

Ah thanks I got it to work! It works perfectly as a workaround thank you!

NikolaSivkov · 4 months ago

Anthropic, i beg of you, please fix this on windows, it's so annoying

CacTye · 4 months ago

Please please fix this. I've tried all the hacks/fixes and they are partial fixes at best. The hooks solution works but only for top level agents; once subagents get involved it breaks again.

slaingod · 4 months ago
Ah thanks I got it to work! It works perfectly as a workaround thank you!

Not sure how you got it to work, mind sharing? The JSON hook stuff is already invalid JSON to being with, missing a brace.

What worked for me:
I was not able to get this to work with WSL. I eventually got it to work after converting the bash script to a local non-WSL js script and have the hook be:

"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node C:/Users/<username...>/.claude/local_hook_tools/no_cd_chaining.js"
},
{
"type": "command",
"command": "node C:/Users/<username...>/.claude/local_hook_tools/no_git_dash_c.js"
}
]
}
]
}

(I am working on Windows projects that actually need windows, so WSL is less useful here....streamdeck plugin and electron app.)

brandonbloom · 4 months ago

Blocking with hooks isn't working for me. The agents are too creative. Block cd ... && ... and it switches to cd ...; .... Block that, and it switches to git -C .... Block that, and it switches to pushd ...; ... block that and it switches to GIT_DIR=... and GIT_WORK_TREE=... - It would be impressive if it wasn't so frustrating.

azlojutro · 4 months ago
Please please fix this. I've tried all the hacks/fixes and they are partial fixes at best. The hooks solution works but only for top level agents; once subagents get involved it breaks again.

I haven't had this issue, I thought that subagents inherit parent hooks?

Blocking with hooks isn't working for me. The agents are too creative. Block cd ... && ... and it switches to cd ...; .... Block that, and it switches to git -C .... Block that, and it switches to pushd ...; ... block that and it switches to GIT_DIR=... and GIT_WORK_TREE=... - It would be impressive if it wasn't so frustrating.

Is the blocking hook providing feedback in what to do like my examples? The behavior you are describing sounds like you are blocking but not providing further guidance.

Kurohyou · 4 months ago

In my opinion this is essentially a breaking bug. Why isn't this a high priority fix!?

edgariscoding · 4 months ago
> Please please fix this. I've tried all the hacks/fixes and they are partial fixes at best. The hooks solution works but only for top level agents; once subagents get involved it breaks again. I haven't had this issue, I thought that subagents inherit parent hooks? > Blocking with hooks isn't working for me. The agents are too creative. Block cd ... && ... and it switches to cd ...; .... Block that, and it switches to git -C .... Block that, and it switches to pushd ...; ... block that and it switches to GIT_DIR=... and GIT_WORK_TREE=... - It would be impressive if it wasn't so frustrating. Is the blocking hook providing feedback in what to do like my examples? The behavior you are describing sounds like you are blocking but not providing further guidance.

Just want to say that I have the hooks configured correctly and it worked for a bit but after a while Claude Code starts trying pushd and other commands. This is despite the fact that I can see it output the hook commands.

CacTye · 4 months ago
I haven't had this issue, I thought that subagents inherit parent hooks?

I thought so too. I setup the hooks, I put very strong instructions in claude.md, nothing works consistently enough to get through a whole superpowers plan. Especially because superpowers loves to make lots of very granular commits.

I should also note for the CC devs that this is happening on WSL, not just windows.

fschwiet · 3 months ago

I haven't seen this issue since I switch to working in a Ubuntu VM sad trombone. That said I do get a bunch of permission requests for other reasons. That's interesting @CacTye is seeing this on WSL. One difference to be mindful of is that Windows filenames are case insensitive and according to google still case insensitive when accessing files from Windows (ie \\mnt\c\...)

yurukusa · 3 months ago

The cd permission prompt on compound commands happens because the permission system parses the first command in the chain, not the actual action.
Hook-based fix that works with subagents:

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 '^cd\s+' && ! echo "$CMD" | grep -qE '(rm\s+-rf|git\s+push\s+--force|git\s+reset\s+--hard)'; 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/cd-git-allow.sh" }]
      }
    ]
  }
}

This auto-approves any compound command starting with cd while still blocking destructive patterns (rm -rf, git push --force, git reset --hard). Since hooks are defined in ~/.claude/settings.json, they apply to all agents including subagents.
Re: subagent issue — hooks in ~/.claude/settings.json (user-level) should inherit to subagents. If they're not firing, check that the hook is in the user settings file, not a project-level .claude/settings.json. You can verify with npx cc-safe-setup --status.

nyalmellor · 3 months ago
The cd permission prompt on compound commands happens because the permission system parses the first command in the chain, not the actual action. Hook-based fix that works with subagents: 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 '^cd\s+' && ! echo "$CMD" | grep -qE '(rm\s+-rf|git\s+push\s+--force|git\s+reset\s+--hard)'; 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/cd-git-allow.sh" }] } ] } } This auto-approves any compound command starting with cd while still blocking destructive patterns (rm -rf, git push --force, git reset --hard). Since hooks are defined in ~/.claude/settings.json, they apply to all agents including subagents. For a more complete setup with 8 safety hooks (including this one), you can run: npx cc-safe-setup Re: subagent issue — hooks in ~/.claude/settings.json (user-level) should inherit to subagents. If they're not firing, check that the hook is in the user settings file, not a project-level .claude/settings.json. You can verify with npx cc-safe-setup --status.

This worked for me

yurukusa · 3 months ago

A PreToolUse hook can parse compound commands and auto-approve when all components are safe:

CMD=$(cat | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$CMD" ] && exit 0
echo "$CMD" | grep -qE '&&|\|\||;' || exit 0
ALL_SAFE=1
while IFS= read -r part; do
    part=$(echo "$part" | sed 's/^\s*//; s/\s*$//')
    [ -z "$part" ] && continue
    if ! echo "$part" | grep -qE '^\s*(cd|ls|pwd|echo|cat|head|tail|wc|sort|grep|find|test|true|mkdir\s+-p)\s|^\s*git\s+(status|log|diff|branch|show|rev-parse|tag|add|commit)\s|^\s*(npm|yarn|pnpm)\s+(test|run|list|audit)\s'; then
        ALL_SAFE=0; break
    fi
done < <(echo "$CMD" | sed 's/&&/\n/g; s/||/\n/g; s/;/\n/g')
if [ "$ALL_SAFE" = 1 ]; then
    jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"compound command auto-approved"}}'
fi
exit 0
yurukusa · 3 months ago

The blocking hook approach from @azlojutro is correct, but can be improved — the deny message can include the extracted real command so Claude retries immediately instead of doing pwd → cd → command as 3 separate calls.
.claude/hooks/no-cd-chaining.sh:

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
if echo "$COMMAND" | grep -qE '^\s*(cd|pushd)\s+\S+\s*(&&|;|\|\|)'; then
  REAL_CMD=$(echo "$COMMAND" | sed -E 's/^\s*(cd|pushd)\s+("[^"]*"|'\''[^'\'']*'\''|[^ &;|]+)\s*(&&|;|\|\|)\s*//')
  cat <<EOF
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Do not prefix commands with cd. Run this directly: $REAL_CMD"}}
EOF
  exit 0
fi
exit 0
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{"type": "command", "command": "bash ~/.claude/hooks/no-cd-chaining.sh"}]
    }]
  }
}

The deny reason includes the stripped command, so Claude immediately retries with just the meaningful part instead of splitting into multiple calls.
For @edgariscoding's internal security check ("Compound commands with cd and git require approval to prevent bare repository attacks") — that fires before hooks. You can safely whitelist cd in the permission system since cd alone doesn't mutate anything:

{
  "permissions": {
    "allow": ["Bash(cd:*)"]
  }
}

The actual dangerous commands (git commit, rm, etc.) still require approval. Combined with the hook above, Claude learns to stop chaining cd after a few blocked attempts, and the cd:* whitelist is a safety net for when it still does.

yurukusa · 3 months ago

Root cause: The permission matcher evaluates the entire compound command as a single string. When Claude runs cd /path && git add file.txt && git commit -m "fix", the matcher sees cd as the base command — not git add or git commit. So even with Bash(git:*) in your allowlist, it won't match.
Hook workaround (PreToolUse):

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$COMMAND" ] && exit 0
echo "$COMMAND" | grep -qE '&&|\|\||;' || exit 0
ALL_SAFE=true
while IFS= read -r part; do
    part=$(echo "$part" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    [ -z "$part" ] && continue
    BASE=$(echo "$part" | awk '{print $1}')
    case "$BASE" in
        cd|pwd|ls|echo|cat|head|tail|grep|test|true|false|[) ;;
        git) ;;
        *) ALL_SAFE=false; break ;;
    esac
done < <(echo "$COMMAND" | sed 's/\s*&&\s*/\n/g; s/\s*||\s*/\n/g; s/\s*;\s*/\n/g')
if [ "$ALL_SAFE" = "true" ]; then
    jq -n '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"All parts of compound command are safe (cd + git)"}}'
fi
exit 0

Add to settings.json:

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

This splits the compound command on &&, ||, ; and checks each part. If all parts are cd, git, or safe builtins, it auto-approves. Otherwise it passes through to the default permission handler.
Same root cause as #16561.

yurukusa · 3 months ago

This is the same underlying issue as #16561 — Claude Code evaluates the entire compound string as one unit, so cd /path && git add file triggers a cd:* permission prompt instead of git:*.

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
CLEAN=$(echo "$COMMAND" | sed 's/#.*$//' | tr '\n' ' ')
PARTS=$(echo "$CLEAN" | sed 's/\s*&&\s*/\n/g; s/\s*||\s*/\n/g; s/\s*;\s*/\n/g; s/\s*|\s*/\n/g')
ALL_SAFE=true
while IFS= read -r part; do
    part=$(echo "$part" | sed 's/^\s*//;s/\s*$//')
    [ -z "$part" ] && continue
    BASE=$(echo "$part" | awk '{print $1}')
    case "$BASE" in
        cd|pushd|popd|pwd) ;;
        cat|head|tail|less|wc|ls|find|which|stat|du) ;;
        grep|rg|sed|awk|sort|uniq|cut|tr|tee|jq) 
            echo "$part" | grep -qE 'sed\s+.*-i' && { ALL_SAFE=false; break; } ;;
        echo|printf|true|false|test|export|set|env|date) ;;
        git)
            SUBCMD=$(echo "$part" | awk '{print $2}')
            case "$SUBCMD" in
                status|log|diff|show|branch|tag|remote|ls-files|rev-parse|blame|add|commit)
                    ;; # add/commit included since they're the intended operation
                *) ALL_SAFE=false; break ;;
            esac ;;
        mkdir|touch) ;;
        *) ALL_SAFE=false; break ;;
    esac
done <<< "$PARTS"
if [ "$ALL_SAFE" = true ]; then
    jq -n '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",permissionDecisionReason:"All compound components are individually safe"}}'
fi
exit 0
// ~/.claude/settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/compound-command-allow.sh" }]
    }]
  }
}

For your specific case (cd /path && git add file && git commit -m "msg"):

  • cd /path → safe (navigation)
  • git add file → safe (staging)
  • git commit -m "msg" → safe (committing)
  • auto-approved, no cd:* prompt

The hook splits on &&/||/;/|, checks each component individually, and only auto-approves when all are known-safe. Unknown commands fall through to the normal permission dialog.
Note on the cd to current directory issue mentioned in comments: Claude Code sometimes prepends cd $(pwd) unnecessarily. The hook handles this too — it just treats the redundant cd as safe and evaluates the real command.

NikolaSivkov · 3 months ago

Since GIT was the biggest offender on this, i downloaded the git mcp form modelcontextprotocol repo, told claude to enhance it a bit, and plugged it into claude itself and now i don't have this problem, it's been couple of days, but it works so far.

dabrady · 3 months ago

Just chiming in that this happens to me on macOS as well.

boognish24 · 3 months ago

this has been going on for 6 weeks now. has anyone from anthropic acknowledged it?

edgariscoding · 3 months ago

I’m guessing they haven’t and probably won’t because they are testing Auto Mode On Apr 8, 2026, at 5:32 PM, Boognish @.***> wrote:boognish24 left a comment (anthropics/claude-code#28240)
this has been going on for 6 weeks now. has anyone from anthropic acknowledged it?

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>

tihomir-kit · 3 months ago

How long would it take Mythos to fix this?

Kalabasa · 3 months ago

Instead of whack-a-moling with allow lists and duplicating the entire permission settings in a bash variable, just deny it:

#!/bin/sh
CMD=$(jq -r '.tool_input.command')

case "$CMD" in
  'cd '*)
    DIR=$(printf '%s' "$CMD" | sed "s/^cd  *//;s/ *[;&|].*//" | tr -d "'\"")
    if [ "$(eval realpath "$DIR" 2>/dev/null)" = "$(pwd -P)" ]; then
      echo "You're already in the same directory, dummy" >&2
      exit 2
    fi
    ;;
esac
bmitioglov · 2 months ago

same thing!

Pjieter · 1 month ago

This is also still an issue for me. Trying to steer it through CLAUDE.md does not work, it will use other similar commands (git -C, Set-Location) on windows 11 with version 2.1.146.

felix314159 · 1 month ago

since you close all other vaguely related issues:
claude constantly chains commands e.g. cd ... && find ... or cd ... ; find ...and even though you specifically whitelist-approved all of these commands in isolation it will keep repeatedly asking for permission to run those. this alone heavily incentizes you to skip all permissions entirely, cuz there are no good workarounds. please fix it, if all commands of a chained command are whitelisted there is NO NEED to ask permission

fschwiet · 1 month ago

I am just going to note that recently I've seen Claude ask for separate permissions for commands that have been &&'d together. But it doesn't always ask, leading me to think they are have a heuristic that detects some joined commands now.

The hooks I currently use is are https://github.com/fschwiet/nosabokit, it is a plugin that also handles chained git commands.

markusa380 · 1 month ago

Still happening for me, despite the explicit instructions:

- MISSION CRITICAL: Do not chain multiple commands unless absolutely necessary:
   - Always run the commands one by one!
   - Never do `cd  ... 2>/dev/null; ...`, avoid the output redirection!
   - Never run `cd ... & git ...` - run cd first, then run git! I repeat, this is critcal: ALWAYS do cd separately first!
markusa380 · 1 month ago

Note, in Claude Desktop there is a reasoning given for these commands requiring permission:

This command changes directory before running git, which can execute untrusted hooks from the target directory. Approve only if you trust it.

It should definitely exclude cases where the repo is in a subdirectory of the working directory, as that obviously is trusted.

markusa380 · 1 month ago

What works for me is this snippet in the .claude/settings.local.json:

"hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash",
        "if": "Bash(cd * && git *)",
        "hooks": [
          {
            "type": "command",
            "command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
          }
        ]
      }
    ]
  }
jamessoubry · 1 month ago

Workaround

Add "Bash(cd *)" to the permissions.allow array in ~/.claude/settings.json:

{
  "permissions": {
    "allow": [
      "Bash(cd *)"
    ]
  }
}

This suppresses the false cd prompt entirely. Any meaningful commands after && that aren't already in the allow list will still get their own permission prompts.

Note on PreToolUse hooks as a bypass mechanism

If you're using a PreToolUse hook (e.g. clawband), hooks that return an explicit permissionDecision: "allow" in their JSON output will bypass Claude Code's native permission check entirely:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "matched allow pattern"
  }
}

This means a well-written PreToolUse hook could intercept compound commands, correctly identify which segment needs permission, and return an explicit allow for the cd portion — bypassing this faulty native check while still evaluating the rest of the pipeline. Worth considering as an alternative fix path.

edgariscoding · 1 month ago

Adding some tested findings, since this thread keeps growing and the popular workarounds don't actually hold up.

TL;DR: the Bash(cd *) / Bash(cd:*) allow-list workaround does not suppress this prompt. I tested it on macOS (so it's not just a Windows path-normalization thing):

  • Added Bash(cd:*) to global settings.json → still prompted.
  • Suspected stale settings, did a full restart → still prompted.
  • Tried the exact Bash(cd *) space-spelling from the upvoted workaround, restarted again → identical prompt.

The reason is that this isn't a normal allow-list miss. The message —

This command changes directory before running git, which can execute untrusted hooks from the target directory. Approve only if you trust it.

— is a separate, hardcoded security gate that doesn't consult the permission allow list at all. It's guarding "is this directory trusted," not "is this command allowed," so no Bash(...) rule of any spelling can silence it. The Bash(cd *) suggestion fixes a different symptom in this thread (the one where cd gets mis-identified as the command prefix). Worth pinning that distinction, because people keep recommending it for the bare-repo case and it quietly does nothing.

Bonus: it fires even when the cd target is a subdirectory of the current working directory — i.e. a path that is, by definition, already trusted. So the gate is protecting me from a repo I'm already sitting inside.

edgariscoding · 1 month ago

Following up on my earlier comment with a workaround that actually holds up — a PreToolUse hook that returns permissionDecision: "allow", which is the only thing I've found that overrides the bare-repo gate instead of racing against it.

The important property: it auto-allows a cd … && … chain only when every segment is read-only. So cd /path && git status runs silently, but cd /path && git commit (or rm, git push, git fetch, a build, anything unrecognized) falls right back to the normal permission prompt. You're suppressing the false prompt without handing the model a blanket pass.

$HOME/.claude/hooks/allow-cd-readonly-chains.sh:

#!/usr/bin/env bash
# PreToolUse hook: auto-allow `cd … && <read-only command>` chains so Claude
# Code's bare-repo "untrusted hooks" gate stops prompting on trusted, NON-mutating
# chains. Anything mutating falls through to the normal permission prompt.

input=$(cat)
cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty')

# Only engage on cd-chained commands; everything else defers to normal flow.
case "$cmd" in
  "cd "*) ;;
  *) exit 0 ;;
esac

# Split on && || ; | and require EVERY segment to be a known read-only prefix.
is_safe() {
  local s="$1"
  s="${s#"${s%%[![:space:]]*}"}"; s="${s%"${s##*[![:space:]]}"}"   # trim
  case "$s" in
    cd|"cd "*|pwd|"pwd "*|ls|"ls "*) return 0 ;;
    "git status"*|"git diff"*|"git log"*|"git show"*|"git rev-parse"*|"git remote -v"*) return 0 ;;
    "cat "*|"head "*|"tail "*|"wc "*|"echo "*|"rg "*|"grep "*|"find "*) return 0 ;;
    "jq "*|"sort"|"sort "*|"uniq"|"uniq "*) return 0 ;;
    *) return 1 ;;
  esac
}

# NOTE: split with awk, not sed. BSD/macOS sed does NOT interpret \n in the
# replacement, so a sed-based split silently fails and the whole chain matches
# the leading "cd " prefix -> it would wrongly ALLOW everything (incl. rm -rf).
all_safe=1
while IFS= read -r seg; do
  [ -z "${seg//[[:space:]]/}" ] && continue
  is_safe "$seg" || { all_safe=0; break; }
done < <(printf '%s' "$cmd" | awk '{gsub(/&&|\|\||;|\|/, "\n")} 1')

if [ "$all_safe" -eq 1 ]; then
  cat <<'JSON'
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "cd chained with read-only commands only"
  }
}
JSON
fi

exit 0

Wire it into ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "bash \"$HOME/.claude/hooks/allow-cd-readonly-chains.sh\"" }
        ]
      }
    ]
  }
}

Setup:

  1. Save the script, then chmod +x ~/.claude/hooks/allow-cd-readonly-chains.sh
  2. Add the hooks block above (requires jq and awk on PATH — both standard on macOS/Linux; on Windows point it at your Git Bash bash).
  3. Restart Claude Code — hooks load at startup.

Verified behavior (macOS):

| Command | Result |
| --------------------------------------------------- | ---------------- |
| cd /path/to/repo && git status --short && echo ok | runs silently ✅ |
| cd /path/to/repo && git diff \| head | runs silently ✅ |
| cd /path/to/repo && git commit -m x | still prompts 🔒 |
| cd /path/to/repo && git fetch | still prompts 🔒 |
| cd /path/to/repo && rm -rf … | still prompts 🔒 |

One gotcha I'll save the next person: do not split the command with sed — BSD/macOS sed doesn't expand \n in the replacement, so the split no-ops and the whole chain matches the leading cd prefix, which means it would happily allow a cd /x && rm -rf ~. awk's gsub(…, "\n") does the right thing cross-platform. Adjust the read-only allowlist in is_safe() to taste.

And yes — this is still a workaround for a gate that over-fires on directories at or below the cwd. Would love for that to just... not need a hook someday. 🙂