feat(cost): 门禁补上 Read —— 覆盖第二大输出源(用户点出「重点是把大输出去掉」)
用户纠正(2026-09-15):① 「12 万 token 就收口」**不是他定的规则**(他选的是"设会话预算"这个方向,
12 万/80 是我自己收紧的取值,未与他确认);② 「切会话」只是**缓解**,**不是重点** ——
他早在更早一轮就点明过:「跟平台参数没关系……**为什么还要在上下文中 如何才能去掉**」。
⇒ 重点 = **让大输出根本不进入历史**,唯一能做到的就是**入口拦截**。而原门禁**只管 Bash** ——
实测 `Read` 是**第二大输出源**且**完全没被覆盖**:某会话 44 次 / 累计 238 K 字符、单条最大 33 K 字符
(≈13 K token);另一会话 136 次 / 217 K 字符。
**改动**
- `bash-output-guard.py`:新增 `READ_BIG = 400 KB` + `read_too_big()`;`tool_name == 'Read'` 时,
目标文件 > 400 KB ⇒ **deny**,文案给出两条等价做法(`Read` 带 `offset`/`limit` 分段读;
或先 `grep -n 关键词 文件 | head -20` 定位行号再读那几行)
- `settings.json`:该 hook 的 matcher `"Bash"` → **`"Bash|Read"`**(⚠️ 配置是启动快照 ⇒ **需重启生效**)
**实测 5/5**:Read 小文件放行 | **Read 大文件拦** | Bash `cat 大文件` 仍拦 | Bash `ls -la` 仍放行 | Write 不受影响
This commit is contained in:
1 parent
c25609c4c1
commit
cb184145e9
1 file changed
+31
-1
@@ -110,6 +110,22 @@ def log(root, detail):
|
||||
|
||||
|
||||
CAT_BIG = 200 * 1024 # `cat` 目标文件 > 200 KB 才值得拦(小文件放行 ⇒ 防日常误伤)
|
||||
READ_BIG = 400 * 1024 # `Read` 目标文件 > 400 KB ⇒ 至少几万 token ⇒ 拦(2026-09-15 实测补)
|
||||
# ↑ 为什么补 Read:本钩子最初只管 Bash,但实测 `Read` 是**第二大输出源** ——
|
||||
# 某会话 44 次 / 累计 238 K 字符、单条最大 33 K 字符(≈13 K token);另一会话 136 次 / 217 K 字符,
|
||||
# 且**完全没有被任何机制覆盖**。⇒ "想把大输出从上下文里去掉",就必须覆盖所有会灌大输出的工具。
|
||||
|
||||
|
||||
def read_too_big(payload):
|
||||
"""`Read` 一个大文件 ⇒ True。取不到大小(文件不存在/相对路径)⇒ False(放行)。"""
|
||||
ti = payload.get('tool_input') or {}
|
||||
p = ti.get('file_path') or ti.get('path') or ''
|
||||
if not isinstance(p, str) or not p:
|
||||
return False
|
||||
try:
|
||||
return os.path.getsize(p) > READ_BIG
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def cat_bigfile(cmd):
|
||||
@@ -169,7 +185,8 @@ def main():
|
||||
return
|
||||
if (payload.get('hook_event_name') or '') != 'PreToolUse':
|
||||
return
|
||||
if (payload.get('tool_name') or '') != 'Bash':
|
||||
tool = payload.get('tool_name') or ''
|
||||
if tool not in ('Bash', 'Read'): # 覆盖**所有**会灌大输出的工具(2026-09-15 由用户点出补 Read)
|
||||
return
|
||||
if os.environ.get('DSH_OUTPUT_GUARD_OFF'):
|
||||
return
|
||||
@@ -178,6 +195,19 @@ def main():
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
if tool == 'Read':
|
||||
if read_too_big(payload):
|
||||
fp = str(((payload.get('tool_input') or {}).get('file_path')) or '')
|
||||
log(str(root), 'DENY|Read 大文件|%s' % fp[:110])
|
||||
_emit({'hookSpecificOutput': {
|
||||
'hookEventName': 'PreToolUse', 'permissionDecision': 'deny',
|
||||
'permissionDecisionReason': (
|
||||
'💰 拦下:**Read 大文件**(`%s`)—— 它的**全文会写进会话历史、之后每轮全量重发**。\n'
|
||||
'✅ 分段读:`Read` 加 `offset` / `limit`(例 `limit: 120`)只取你要的那段;'
|
||||
'或先 `grep -n "关键词" 文件 | head -20` 定位行号,再读那几行。\n'
|
||||
'ℹ️ 确实要通读 ⇒ 用 Bash 分批 `sed -n \'1,200p\' 文件`;急停 env `DSH_OUTPUT_GUARD_OFF=1` '
|
||||
'或 `.workbuddy/bash-guard.disabled`。' % fp[:80])}})
|
||||
return
|
||||
cmd = ((payload.get('tool_input') or {}).get('command')) or ''
|
||||
if not cmd or SAFE.search(cmd):
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user