工具大全
亲手实测作者:Coocon2026年8月6日469 次阅读约 3 分钟阅读

Claude Code Hooks 的 stdin 陷阱:python heredoc 会吃掉你的 hook JSON

官方文档怎么说

Claude Code 的 hooks 机制很直接:在 settings.json 里注册一条命令,事件(比如 PostToolUse)触发时,Claude Code 会启动这个命令,并把事件的 JSON 数据通过 stdin 传给它。文档原话很简洁——hook 从 stdin 读 JSON,处理,按需返回退出码。

看起来是 Unix 管道的标准玩法,实现一个"自动记录失败命令"的 hook 应该十分钟搞定。

实测发生了什么

我要做的事:每当 Bash 命令失败,把它记到 tasks/lessons-inbox.md 收件箱里(去重、过滤良性失败)。逻辑不复杂但 shell 写起来啰嗦,自然的选择是让 python 干活,于是第一版 hook 长这样:

#!/bin/sh
python3 - <<'PYEOF' 2>/dev/null
import json, sys

data = json.load(sys.stdin)   # 读 hook 传来的 JSON —— 你以为
cmd = data.get("tool_input", {}).get("command", "")
# ... 判定失败、写收件箱 ...
PYEOF
exit 0

跑起来毫无报错。然后连续触发了好几条注定失败的命令——收件箱文件始终是空的。没有报错、没有日志、settings.json 配置检查了三遍没问题,hook 就是"没生效"。

坑在哪:heredoc 重定向了 stdin

拆开这条命令的数据流就清楚了:

  1. Claude Code 启动 hook 进程,把事件 JSON 接到进程的 stdin 上
  2. python3 - 的意思是"从 stdin 读程序本身"
  3. <<'PYEOF' heredoc 把 python 的 stdin 重定向成了 heredoc 的内容——也就是那段 python 脚本

于是 python 的 stdin 被脚本文本占满,读完程序后已是 EOF。脚本里的 json.load(sys.stdin) 读到空输入,抛 JSONDecodeError——而这个异常被 2>/dev/null 和"hook 永不阻塞"的容错设计完整吞掉了。Claude Code 侧看到 hook 正常退出(exit 0),一切"成功"。

真正的 hook JSON 呢?它还挂在外层 sh 进程的 stdin 上,但 heredoc 生效后 python 已经不可能读到它了。

这个坑的恶心之处是三层静默的叠加:heredoc 语义上完全合法(shell 不报错)、python 异常被重定向吞掉(运行时不报错)、hook 按最佳实践 exit 0(Claude Code 不报错)。每一层单独看都是正确设计,叠在一起就是一个无声的黑洞。

修复:先落盘,再传路径

修复只要两行——在 python 启动之前,先用 cat 把外层进程的 stdin 消费掉、落到临时文件,再把文件路径通过环境变量传进去:

#!/bin/sh
TMP_IN="$(mktemp)" || exit 0
cat > "$TMP_IN" 2>/dev/null || true          # 先把 hook JSON 从 stdin 接下来
CL_HOOK_INPUT="$TMP_IN" python3 - <<'PYEOF' 2>/dev/null
import json, os, sys

try:
    data = json.load(open(os.environ["CL_HOOK_INPUT"], encoding="utf-8"))
except Exception:
    sys.exit(0)

if data.get("tool_name") != "Bash":
    sys.exit(0)
# ... 失败判定、良性过滤、按命令哈希去重、追加收件箱 ...
PYEOF
rm -f "$TMP_IN"
exit 0

改完立刻生效:失败命令一条条落进收件箱,去重和过滤都正常。

顺带附上这个 hook 完整版里几个值得抄的设计(都是实际跑了一段时间验证过的):

  • 永不阻塞:任何异常路径都 exit 0,包括 mktemp 失败——hook 挂了不能连累主流程
  • 良性失败过滤:grep / rg / diff / test 非零退出是正常语义,不记
  • 按命令哈希去重:同一条命令反复失败只记一次,收件箱不膨胀

适用边界

  • 只影响"hook 脚本内用 heredoc 给解释器喂脚本"的写法——python3 - <<EOF、node - <<EOF 同理都会中招;如果你的 python 脚本是独立文件(python3 hook.py),stdin 原样传递,没有这个问题
  • python3 -c '一行代码' 不占 stdin,短逻辑可以用它绕开;但超过几行就不可维护了,先落盘方案更稳
  • 本文实测于文首标注的环境;hooks 通过 stdin 传 JSON 是文档明确的稳定契约,此坑源于 shell 语义而非 Claude Code 版本行为,预期长期有效

通用教训

hook 这类"配角代码"通常被要求静默容错——这没错,但开发调试期请先把 2>/dev/null 摘掉。这次如果早点看到 JSONDecodeError: Expecting value,十分钟就能定位;带着全套静默设计去调试,多花了一个数量级的时间。容错是给生产的,不是给排障的。

这类实测,每周六汇总一封

订阅码农早餐:每天 8:00 一封 AI 编程早报,每周六另附本周 Claude Code / Codex / 本地模型的实测和踩坑汇总。

相关文章

Claude Code 禁止 rm -rf 怎么配:deny 规则两周拦下 14 次,实测还有 7 种写法照样能删,一个 hook 补齐

为了防 Claude Code 误删文件,我在全局 settings 里加了一条 deny 规则 Bash(rm -rf *)。翻本机 10 月 7 日到 21 日的会话记录:这条规则拦下 14 次删除,模型被拒后 8 次换新目录名、6 次直接放弃删除,一次都没换写法绕过。但在 Claude Code 2.1.285 上用 stub 逐字回放 22 种删除写法后发现:rm -fr、rm -r -f、rm -Rf、rm -r 在手动、auto、bypass 三种模式下都照删不误,/bin/rm、find -delete、bash -c 在手动和 bypass 模式下也能删。auto 模式下,工作目录内的 rm -fr 会被当成「acceptEdits 也会放行」的操作,连分类器都不过。另一道闸是内置的 dangerous-rm 检查,目标是 $D/* 这类变量展开时触发,bypass 模式也拦;多行命令里前面有 heredoc,它就跟不上同一条命令里的变量赋值。文末给出一个 PreToolUse hook,三种模式下补齐全部递归删除写法,并告诉模型被拒后该怎么做。

claude-codehooks+5
pitfalls2026年10月21日7 min
211

Claude Code 接 DeepSeek / GLM / Kimi 开 auto 模式:temporarily unavailable 括号里的原因怎么看,哪些重试没用(故障注入实测)

用第三方模型跑 Claude Code auto 模式,每个有副作用的动作都会用你的会话模型向同一个后端多发一次判定请求:系统提示词 132,470 字符,单次约 2.97 万输入 token,比主循环那一步还多。判定请求一失败,就出现「xxx is temporarily unavailable, so auto mode cannot determine the safety of Bash」。本文用故障注入代理把每种失败对应的报错原文测了一遍:500 → (server error)、429 → (rate-limited)、529 → (overloaded)、断连 → (connection failed),这几种会自动重试 5 次;超过 60 秒没回 → (timed out);**没有括号 = 后端拒收(4xx),只试 1 次,重试多少遍都一样**;后端回了但模型没给判定 → could not evaluate,连续 3 次就退回人工确认。另外实测 CLAUDE_CODE_AUTO_MODE_MODEL 不生效,判定器只能跟着会话模型换。Claude Code 2.1.285,真实后端 DeepSeek v4-pro 与小米 MiMo v2.5。

claude-codedeepseek+7
pitfalls2026年10月11日11 min
645

Claude Code 上下文太长怎么办:Prompt is too long 会自动压缩,中转站 / DeepSeek 的 maximum context length 却不会(实测)

Claude Code 靠报错文案判断「上下文太长」:后端说 prompt is too long 或 input is too long for requested model,它会自动压缩对话后重发,用户无感;DeepSeek 和 OpenAI 风格中转返回的 This model's maximum context length is …,它不认识,直接报 API Error: 400,每轮都失败。手动 /compact 有效;更好的办法是用 CLAUDE_CODE_MAX_CONTEXT_TOKENS(非 claude- 模型 ID)或 CLAUDE_CODE_AUTO_COMPACT_WINDOW(claude- 模型 ID)告诉它真实上限,让它提前压缩。Claude Code 2.1.285 + 本地 stub 实测。

claude-codedeepseek+6
pitfalls2026年10月5日8 min
727

Claude Code 一直卡住、转圈没反应怎么办:后端不回时它要等 6 分钟,重试满 10 次可能卡一个多小时(实测)

Claude Code 转圈不动、或者停在 Retrying in 0s,多半是后端没有回数据。实测 2.1.285(API key + ANTHROPIC_BASE_URL):后端不回响应头时每次等 6 分钟(360 秒)才判超时,把 API_TIMEOUT_MS 调到 60 万、90 万也不变,只能调小;默认重试 10 次,总共可能卡一个多小时。中转站如果把回复缓冲到生成完才返回,超过 6 分钟的回复永远拿不到。按 Esc 可以随时中断。全部在本地 stub 上实测。

claude-code中转站+6
pitfalls2026年10月5日10 min
650