claude-code - 💡(How to fix) Fix [BUG] `normalizeToolInput` strips `cd <cwd> &&` from Bash commands before PreToolUse hooks can inspect them

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

The model enters an unrecoverable retry loop. Example from a real session (the only replaced part is the command, where I've put dangerous_command):

● Bash(dangerous_command -auto-approve 2>&1) ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● Bash(dangerous_command -auto-approve 2>&1) ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● Bash(dangerous_command -auto-approve 2>&1) ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● I keep hitting the same hook — it requires the command to start with cd. Let me try with the explicit cd && prefix.

● Bash(dangerous_command -auto-approve 2>&1) ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● I'm stuck in a loop where the cd prefix keeps getting stripped from my command before it reaches the tool. I genuinely can't get past this hook.

The model is correct — it IS generating the cd prefix, but normalizeToolInput removes it. The session JSONL also records the post-normalized command, so debugging the transcript makes it look like the model never included cd when it did.

Root Cause

  1. Start Claude Code in any directory (e.g., /tmp/testdir)
  2. Add a PreToolUse hook that requires cd for a specific command:
    {
      "hooks": {
        "PreToolUse": [{
          "matcher": "Bash",
          "hooks": [{
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'echo hello'; then if ! echo \"$CMD\" | grep -qE '^cd '; then echo 'Blocked: must use cd prefix' >&2; exit 2; fi; fi"
          }]
        }]
      }
    }
  3. Ask Claude: "Run this exact command: cd /tmp/testdir && echo hello"
  4. The hook rejects it because it receives echo hello (the cd /tmp/testdir && was stripped)
  5. Claude retries in a loop, never able to satisfy the hook

Code Example

// from the compiled binary (deobfuscated variable names)
let parsedInput = BashTool.inputSchema.parse(rawInput);
let { command, timeout, description } = parsedInput;
let cwd = getCwd();
let normalizedCommand = command.replace(`cd ${cwd} && `, "");

---

The model enters an unrecoverable retry loop. Example from a real session (the only replaced part is the command, where I've put `dangerous_command`):


Bash(dangerous_command -auto-approve 2>&1)
Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

Bash(dangerous_command -auto-approve 2>&1)
Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

Bash(dangerous_command -auto-approve 2>&1)
Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

I keep hitting the same hook — it requires the command to start with cd.
  Let me try with the explicit cd && prefix.

 Bash(dangerous_command -auto-approve 2>&1)
Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

I'm stuck in a loop where the cd prefix keeps getting stripped from my command
  before it reaches the tool. I genuinely can't get past this hook.


The model is correct — it IS generating the `cd` prefix, but `normalizeToolInput` removes it. The session JSONL also records the post-normalized command, so debugging the transcript makes it look like the model never included `cd` when it did.

---

{
     "hooks": {
       "PreToolUse": [{
         "matcher": "Bash",
         "hooks": [{
           "type": "command",
           "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'echo hello'; then if ! echo \"$CMD\" | grep -qE '^cd '; then echo 'Blocked: must use cd prefix' >&2; exit 2; fi; fi"
         }]
       }]
     }
   }
RAW_BUFFERClick to expand / collapse

Preflight Checklist

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

What's Wrong?

The normalizeToolInput function for the Bash tool contains this logic:

// from the compiled binary (deobfuscated variable names)
let parsedInput = BashTool.inputSchema.parse(rawInput);
let { command, timeout, description } = parsedInput;
let cwd = getCwd();
let normalizedCommand = command.replace(`cd ${cwd} && `, "");

This strips cd <current_working_directory> && from the command before PreToolUse hooks receive tool_input.command and before the command is logged to the session JSONL.

Some tools are directory-sensitive — they perform different, potentially destructive actions depending on which directory they run in (e.g., IaC tools that apply infrastructure changes, package managers that modify different projects, database migration tools). A monorepo or multi-module layout might have dozens of these directories side by side.

A PreToolUse hook that requires cd <dir> && <command> serves as an intent declaration: the model must name the exact directory it wants to operate in, and the human reviewing the permission prompt can verify that directory is correct before approving. Without the explicit cd, a bare <command> runs in whatever the cwd happens to be — which may be a different module than the model (or the user) intended, with no signal in the approval prompt that anything is wrong.

The cd prefix is not about ensuring the shell is in the right directory (the cwd may already be correct). It's about making the model's intent visible and auditable at approval time.

The normalizer defeats this by silently removing the declaration before it reaches the hook or the user.

What Should Happen?

PreToolUse hooks should receive the original, un-normalized command so they can inspect the model's full intent. The normalization can still be applied for actual shell execution, but hooks need the raw input to make informed decisions.

Alternatively, normalizeToolInput should not modify tool_input.command at all — the cd is semantically meaningful even when redundant for execution, and stripping it has observable side effects (breaking hooks, misleading transcripts).

Error Messages/Logs

The model enters an unrecoverable retry loop. Example from a real session (the only replaced part is the command, where I've put `dangerous_command`):


● Bash(dangerous_command -auto-approve 2>&1)
  ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● Bash(dangerous_command -auto-approve 2>&1)
  ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● Bash(dangerous_command -auto-approve 2>&1)
  ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● I keep hitting the same hook — it requires the command to start with cd.
  Let me try with the explicit cd && prefix.

● Bash(dangerous_command -auto-approve 2>&1)
  ⎿ Error: PreToolUse:Bash hook error: Blocked: must be preceded by 'cd <dir> &&'

● I'm stuck in a loop where the cd prefix keeps getting stripped from my command
  before it reaches the tool. I genuinely can't get past this hook.


The model is correct — it IS generating the `cd` prefix, but `normalizeToolInput` removes it. The session JSONL also records the post-normalized command, so debugging the transcript makes it look like the model never included `cd` when it did.

Steps to Reproduce

  1. Start Claude Code in any directory (e.g., /tmp/testdir)
  2. Add a PreToolUse hook that requires cd for a specific command:
    {
      "hooks": {
        "PreToolUse": [{
          "matcher": "Bash",
          "hooks": [{
            "type": "command",
            "command": "CMD=$(jq -r '.tool_input.command'); if echo \"$CMD\" | grep -qE 'echo hello'; then if ! echo \"$CMD\" | grep -qE '^cd '; then echo 'Blocked: must use cd prefix' >&2; exit 2; fi; fi"
          }]
        }]
      }
    }
  3. Ask Claude: "Run this exact command: cd /tmp/testdir && echo hello"
  4. The hook rejects it because it receives echo hello (the cd /tmp/testdir && was stripped)
  5. Claude retries in a loop, never able to satisfy the hook

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

No response

Claude Code Version

2.1.150 (Claude Code)

Platform

Anthropic API

Operating System

Ubuntu/Debian Linux

Terminal/Shell

Other

Additional Information

Related issues:

  • #43189 — Same symptom (model loops on cd hook), closed without root cause identified. The reporter and the model both suspected "conflicting instructions" but the actual cause is normalizeToolInput.
  • #13150 — Same class of bug (normalizeToolInput stripping -- from commands)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING