Claude Code Bash 工具卡死:修复挂起命令

dev server 或 curl 一直跑、Claude Code 卡在转圈。用 run_in_background、显式 timeout、再用 /tasks 杀掉卡住的 shell。2026 年 6 月实测。

Claude Code 跑了 npm run dev,然后等啊等啊等。dev server 起来了、一直跑着不退,于是 agent 以为命令”还没结束”、session 卡在一个转圈的指示器上。或者 Bash(curl https://flaky-api.com) 卡在 TCP 读上,因为对端接受了连接却从不发数据。或者脚本进了死循环,模型耐心等着永远不会来的 stdout。session 活着,但没用。

最快的修复:如果这条命令是 server、watcher、或任何本来就该一直跑的东西,就别在前台跑。要么在它运行时按 Ctrl-b 把它移到后台,要么让 Claude 用 run_in_background: true 来跑。Claude 会立刻拿回控制权、之后再去读捕获到的日志。如果是有限但很慢的任务(测试套件、大构建),那就在 Bash 调用里传一个显式的 timeout。本文剩下的部分讲怎么判断你落在哪一类,以及怎么救一个已经卡死的 shell。

Bash 工具的超时到底怎么工作(2026 年 6 月)

按官方 Claude Code 工具参考,每条 Bash 命令受两个上限约束:

  • 超时:默认 2 分钟(120000 ms)。 Claude 可以通过传 timeout 参数(单位毫秒)把单条命令请求到最多 10 分钟。你可以在 settings.json 里用 BASH_DEFAULT_TIMEOUT_MSBASH_MAX_TIMEOUT_MS 这两个环境变量来调整默认值和上限。
  • 输出长度:默认 30,000 字符,硬上限是 150,000(用 BASH_MAX_OUTPUT_LENGTH 调)。超过限度后,Claude Code 会把完整输出写进一个文件、把路径加一段开头预览交给 Claude。

超时触发时,命令会被杀掉,Claude 看到的是一个 timeout 结果、而不是正常的 exit code。这正是 agent 开始”犯迷糊”的时刻:server 并没有失败,它只是永远不返回,所以 agent 没东西可以接着处理。

对真正的长跑进程(dev server、watch 构建),官方给的答案是 run_in_background: true:它把命令作为后台任务启动、立刻把控制权还回来。用 /tasks 这个 slash 命令来列出和停止后台任务(旧版本里它叫 /bashes)。

常见原因

1. 长跑命令在前台跑

npm run devnext devvitepython -m http.servertail -f——这些设计上就是一直跑、只能 Ctrl-C 才停。在 Bash 调用里前台跑会撞上 2 分钟超时、把 agent 卡住。

如何判断:卡住的命令是 server 或 watcher。输出里有 Server running on...ready in 800 ms、或 Local: http://localhost:3000,然后就没声音了。

2. 默认 2 分钟对这个任务太短

真长但有界的任务(完整测试套件、大构建、800 个包的 npm install)合理就需要 5 分钟以上。工具超时、agent 以为失败了、重试、再吃更多时间。

如何判断:任务确实长但有上界。工具返回的是一条 timeout 信息,而不是命令报错或非零 exit code。

3. 网络调用打到一个不响应的端点

curlwgetgit fetchgit clone 打到一个接受 TCP 连接但从不发数据的端点。socket 一直开着、工具把整个超时耗满。

如何判断:命令是网络相关的。没有进度输出、也没有报错。kill 掉重试经常就好,或者在另一个终端里能跑通。

4. 命令在等 stdin

有些命令没被 pipe 时会从 stdin 读:不带 -mgit commitgh auth login、REPL 启动(pythonnode)、第一次连新主机要确认指纹的 ssh、shell 里调 read 的任何东西。工具没有可交互的终端去回应这个提示。

如何判断:这条命令在真终端里会向人提示。没有输出、无限等待。

5. 子进程或 daemon 把父进程吊着

不带 -ddocker compose up、一个派生 watcher 的 postinstall 脚本、或任何 fork 到后台但父进程还挂在一个 pipe 上的脚本。

如何判断:开头有输出、然后卡住,虽然工作”看起来做完了”。是某个子进程把父进程的 stdout 撑着没关。

6. hook 包裹了命令、阻塞了

一个会卡的 PreToolUsePostToolUse hook(没设超时的网络调用、它自己向人要输入)会阻塞整个工具调用。命令本身可能瞬间就跑完了。

如何判断:工具调用开始了、但完全没有命令输出。如果你在 ~/.claude/settings.json.claude/settings.json 里装过自定义 hook,先怀疑这个。

我落在哪一类?

先分类、再修。每一类的恢复路径都不一样。

类别例子症状修法
有界(会结束)测试、构建、npm install、迁移还没跑完就返回 timeout加大 timeout,或后台跑 + 轮询
无界 serverdev server、watcher、daemon、tail -f有输出后沉默、永不返回run_in_background: trueCtrl-b
卡在网络打到死端点的 curlgit fetch无输出、无报错、一直等kill 掉、加 --max-time、重试
等 stdingit commit、REPL、gh auth login本会向人提示、却静默着用非交互 flag
被 hook 阻塞装了自定义 hook 时的任何命令工具启动、零命令输出禁用 hook、收窄它的 matcher

需要收集的信息

  • 卡住的 Bash 命令原文,含任何包装。
  • 进入沉默前打过的输出。
  • 你的 Claude Code 版本(claude --version)——超时行为在各个版本之间变过。
  • 同样的命令在普通终端里跑能不能完成。
  • ~/.claude/settings.json 或项目 .claude/settings.json 里有没有自定义 hook。
  • 卡住时的进程树,从另一个终端:ps -ef | grep <command>

分步修复

Step 1:判断这命令到底会不会结束

用上面那张表。一定要先分对——把一个有限的测试跑放后台会藏掉它的 exit code,而给 dev server 加大超时只是把同一个 hang 往后拖。

Step 2:有界长任务,传一个显式 timeout

让 Claude 在 Bash 调用上设 timeout 参数,单位毫秒:

用 10 分钟超时跑测试套件。

Claude 会发出一个等价于 Bash(command="npm test", timeout=600000) 的调用。把它设成预期运行时长的大约 2–3 倍,这样一次合理的慢跑不会被杀掉。单条命令上限是 10 分钟;要把一个项目里每条命令的默认值都抬高,就在 settings.json 里设:

{
  "env": {
    "BASH_DEFAULT_TIMEOUT_MS": "300000",
    "BASH_MAX_TIMEOUT_MS": "900000"
  }
}

已知坑(截至 2026 年 6 月):某些版本会忽略 BASH_DEFAULT_TIMEOUT_MS / BASH_MAX_TIMEOUT_MS、强制执行一个更低的硬上限(记录在 anthropics/claude-code#34138)。如果一个任务总是远在你配置的上限之下就被杀,就把它当无界处理、改用 Step 4 的 nohup 办法。

Step 3:server 和 watcher,放后台跑

这是大多数人漏掉的那一步。不要手动重定向到日志文件再追加 &——Claude Code 有原生后台模式:

  • 当一条前台命令在跑时,按 Ctrl-b 把它移到后台。(在 tmux 里要按两次 Ctrl-b,因为 tmux 也占用这个前缀键。)
  • 或者让 Claude 一开始就以后台方式启动:“把 dev server 放后台启动。” Claude 会用 run_in_background: true 跑这个 Bash 调用、拿到一个 task ID、立刻把控制权还回来。

然后不用碰另一个终端就能查看或停止它:

# 列出运行中/已完成的后台 shell,带 ID、状态、运行时长、exit code:
/tasks            (旧版本:/bashes)

# 让 Claude 读某个后台任务捕获到的输出,或停掉它:
"把 dev server 任务最后 30 行给我看看。"
"停掉后台跑着的 dev server。"

底层 Claude 会读这个任务的输出文件、用 TaskStop 工具杀掉它。server 在后台跑的时候,session 还能干别的活。

Step 4:硬上限 bug 的兜底——nohup + 轮询

如果一个有界任务总在你配置的超时之下被杀(Step 2 里那个 bug),就用 nohup 把它从工具里脱出来、跨两次独立的 Bash 调用轮询一个哨兵文件:

# Call 1 —— 必须立刻退出:
nohup bash -c 'npm test > /tmp/test.log 2>&1; echo $? > /tmp/test.exit' &

# Call 2 —— 轮询到 exit-code 文件出现,再看输出:
while [ ! -f /tmp/test.exit ]; do sleep 5; done; echo "exit=$(cat /tmp/test.exit)"; tail -40 /tmp/test.log

第一个调用瞬间返回;第二个只阻塞在便宜的 sleep 循环上,而这个循环本身因为每轮都打印、所以保持在超时之内。跑完记得清掉临时文件。

Step 5:往 CLAUDE.md 加一条 hang 检测说明

让 Claude 识别并暴露 hang,而不是静默重试。加进 CLAUDE.md

对任何 Bash 命令:如果约 60 秒没输出、且这条命令不是已知的长跑型,那就:
1. 停下来告诉我。
2. 建议 kill 掉进程、换条路试。
3. 不要静默重试同一个卡住的命令。

对有限的长任务(测试、构建),一律传显式 timeout。
对 server 和 watcher,一律用 run_in_background: true 跑。
对网络调用,一律带超时 flag(curl --max-time 等)。

Step 6:杀掉一个已经卡死的 shell

当某个东西此刻就卡着时:

# 在 Claude Code session 里:
- 按 Esc 中断当前工具调用。
- 运行 /tasks(旧版本:/bashes),从菜单里停掉卡住的 shell,
  或者在 3 秒内按两次 Ctrl-x Ctrl-k 杀掉所有后台 shell。

如果之后 OS 进程还活着(比如一个 detach 出去的子进程),就从另一个终端杀:

ps -ef | grep <command>
kill -TERM <pid>     # 优雅
kill -KILL <pid>     # TERM 被忽略时

然后告诉 Claude 这条命令失败了,让它换条路、而不是再重试。

Step 7:排查阻塞的 hook

如果工具调用开始了、却完全没有命令输出,多半是 hook。临时禁用 hook 再重试:

mv ~/.claude/settings.json ~/.claude/settings.json.bak
# 重启 Claude Code、重跑命令

如果现在能跑了,就是 hook 在阻塞。把文件恢复回来、收窄 hook 的 matcher 让它只在需要的命令上跑,并给 hook 里任何网络调用加上超时。

怎么确认已经修好

  • 原本卡住的命令现在要么在它的超时内完成、要么后台跑并立刻把控制权还回来、要么被识别为卡死并报给你。
  • /tasks 把后台 server 显示为运行中、有一个在涨的运行时长,你随时能读它的输出。
  • Claude Code 不再静默重试卡住的命令。
  • server 后台跑的时候 session 还能干别的活。
  • 你停掉一个任务后,/tasks 把它显示为已停止,ps -ef | grep <command> 也查不到东西了。

长期预防

  • 任何跑 server、watcher、daemon 的东西,默认用 run_in_background: true(或 Ctrl-b)。
  • 任何预计超过约 30 秒的有限命令,传显式 timeout;项目里全是慢任务的话,就在 settings.json 里设 BASH_DEFAULT_TIMEOUT_MS
  • 网络调用一律带超时 flag:curl --max-time 30wget --timeout=30git -c http.lowSpeedLimit=1 -c http.lowSpeedTime=30 fetch
  • agent 上下文里别用读 stdin 的命令——参数靠 flag 传(git commit -m "msg"gh auth login --with-tokenssh -o BatchMode=yes)。
  • hook 尽量少、matcher 尽量窄、自带超时,这样它们不会阻塞无关工作。
  • 用完就清掉后台 shell,免得最后留着一堆泄漏的 dev server。

常见坑

  • 让 agent 把同一个卡住的命令连着重试三次——浪费上下文、永远恢复不了。
  • timeout 设得太紧(比如 60000)卡住合法的长任务;假 hang 和真 hang 一样难受。
  • 前台跑 npm run dev、然后惊讶 agent 锁住了。
  • 忘了停后台 shell,电脑上最后留着几个占着端口的泄漏 server。
  • 在前台工具调用里用 tail -f”看实时进度”——这等于自找 hang。放后台、或者读一次日志文件。

FAQ

Q:Claude Code 的 Bash 默认超时是多少? A:截至 2026 年 6 月,单条命令默认 2 分钟(120000 ms)。Claude 可以通过传 timeout 参数请求到最多 10 分钟;用 settings.json 里的 BASH_DEFAULT_TIMEOUT_MSBASH_MAX_TIMEOUT_MS 改默认值和上限。

Q:怎么把一条已经在跑的命令移到后台? A:在它运行时按 Ctrl-b(tmux 里按两次)。Claude 会把进程作为后台任务保活、并拿回控制权。你也可以让 Claude 一开始就用 run_in_background: true 启动它。

Q:Claude 看得到后台进程的输出吗? A:看得到。Claude Code 会把后台任务的输出捕获到一个文件;Claude 按需读它,或者你运行 /tasks(旧版本:/bashes)看状态、运行时长、exit code。它不会自动把输出流进对话里。

Q:我的 npm test 就算设了大超时也总在约一分钟后挂掉,为啥? A:某些版本会忽略超时设置、强制一个更低的硬上限(见 anthropics/claude-code#34138)。用 nohup 把任务脱出来、在单独的 Bash 调用里轮询哨兵文件(Step 4),或者升级 Claude Code 再试。

Q:如果命令需要交互输入(密码、确认)怎么办? A:agent 上下文里别用交互命令。用非交互 flag(--yes--no-input-m)、把凭据预设在环境变量或配置文件里,并传 ssh -o BatchMode=yes,这样指纹提示会快速失败而不是挂住。

Q:怎么一次杀掉所有卡住的后台 shell? A:在 session 里 3 秒内按两次 Ctrl-x Ctrl-k 杀掉每个后台 shell,或者打开 /tasks 逐个停掉。

相关阅读

标签: #Claude Code #agent #排查