@chrono-meta/fh-gate 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/regression/probes_live.yaml +137 -0
- package/.claude/rules/.residency-patterns.defaults +7 -0
- package/.claude/rules/fh_4axis_gate.md +50 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +12 -2
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +37 -5
- package/README.zh.md +45 -8
- package/docs/STANDARDS_ALIGNMENT.md +61 -0
- package/docs/USER_GUIDE.md +3 -0
- package/docs/USE_CASES.md +50 -0
- package/docs/model_tier_expectations.md +60 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +451 -4
- package/package.json +40 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/README.md +4 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
- package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
- package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
- package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
- package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
- package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
- package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
- package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +76 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-qp/README.md +71 -0
- package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
- package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
- package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
- package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
- package/plugins/fh-qp/qp_profile.example.yaml +29 -0
- package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
- package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
- package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
- package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
- package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
- package/scripts/chamber_run.sh +14 -5
- package/scripts/com.forge-harness.live-eval.plist +84 -0
- package/scripts/compaction_probe.sh +9 -35
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/map_postprocess.py +90 -0
- package/scripts/outbound_query_guard.sh +131 -0
- package/scripts/outbound_query_hook.sh +373 -0
- package/scripts/package_coverage_check.sh +55 -16
- package/scripts/pipe_verdict_guard.sh +41 -1
- package/scripts/probe_live_eval.sh +240 -0
- package/scripts/probe_live_eval_lib.py +579 -0
- package/scripts/proposal_hook.sh +120 -17
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/selfcheck.sh +41 -1
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +98 -2
- package/scripts/test_action_yml_lanes.sh +160 -0
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_marker_affected_lanes.sh +93 -0
- package/scripts/test_marker_crossfamily_lanes.sh +90 -6
- package/scripts/test_marker_oracle_lanes.sh +136 -0
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +3 -3
- package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
- package/scripts/test_probe_live_eval_lanes.sh +437 -0
- package/scripts/test_proposal_hook_lanes.sh +22 -1
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_sim_isolated_run_lanes.sh +119 -0
- package/scripts/test_utterance_intake_lanes.sh +414 -0
- package/scripts/test_worktree_reclaim_lanes.sh +70 -0
- package/scripts/transcript_utterances.py +222 -0
- package/scripts/utterance_intake.sh +424 -0
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +353 -1
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +65 -1
|
@@ -42,6 +42,97 @@ REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
|
42
42
|
[ -z "$REPO_ROOT" ] && { echo " ❌ pre-push: not in a git repo — fail-closed (block)"; exit 1; }
|
|
43
43
|
ZERO="0000000000000000000000000000000000000000"
|
|
44
44
|
PUSH_REMOTE="${1:-origin}" # git passes the remote NAME as $1; ranges are scoped to it (see below)
|
|
45
|
+
|
|
46
|
+
# ── Push zone: a non-owner github.com remote is not a plain-push channel ─────────────────────
|
|
47
|
+
# CLAUDE.local.md §REST API push 계정규칙: a push under a non-owner account goes through the REST
|
|
48
|
+
# Contents API (new files only, never overwrite/delete) — plain `git push` is not that channel.
|
|
49
|
+
# That rule was correctly applied to ONE org repo and missed on ANOTHER org repo in the SAME round
|
|
50
|
+
# (2026-07-26) — not from ignorance, but because it was applied at the point of discovery instead
|
|
51
|
+
# of enumerated across every remote in play (the half-fix / propagation-boundary class).
|
|
52
|
+
# `scripts/push_zone_check.sh` closes the ENUMERATE half by hand, before a push round, and it
|
|
53
|
+
# explicitly does not block. This block closes the other half mechanically: it cannot see the whole
|
|
54
|
+
# round, but it CAN refuse the one push in front of it when THIS push's own remote sits outside the
|
|
55
|
+
# owner list — exactly the "forgot on this one" shape of the 2026-07-26 miss.
|
|
56
|
+
#
|
|
57
|
+
# Degrade direction (Surface-Class Degrade Invariant): applicability is mechanical, never guessed.
|
|
58
|
+
# • no owner-account list → UNCALIBRATED, plain push allowed (a guessed verdict could
|
|
59
|
+
# authorise the wrong channel, which is worse than no verdict at all)
|
|
60
|
+
# • remote host is not github.com → out of scope for this axis (the account rule is a github.com
|
|
61
|
+
# personal-account axis; a GHE / other host remote says so and is allowed, not silently risky)
|
|
62
|
+
# • remote URL does not parse into a known shape → UNMEASURED, allowed (a harness gap, not a
|
|
63
|
+
# policy decision — NOT the destructive-branch UNCLASSIFIABLE case below, which fails closed
|
|
64
|
+
# because a delete is irreversible; blocking every unfamiliar remote-URL shape here would just
|
|
65
|
+
# train --no-verify on this same hook's real, irreversible guards)
|
|
66
|
+
# • owner ∈ list → silent (the common, expected case)
|
|
67
|
+
# • owner ∉ list → BLOCK. Never prints the owner list itself — only the single non-matching
|
|
68
|
+
# owner already public in the URL being pushed to.
|
|
69
|
+
#
|
|
70
|
+
# Override (explicit, logged — same shape as DESTRUCTIVE_OP_OK / PUBLIC_SURFACE_OK):
|
|
71
|
+
# PUSH_ZONE_OK=1 git push …
|
|
72
|
+
PUSH_ZONE_SRC="${PUSH_ZONE_OWNERS:-$REPO_ROOT/.claude/rules/.push-zone-owners}"
|
|
73
|
+
if [ ! -f "$PUSH_ZONE_SRC" ]; then
|
|
74
|
+
echo " ⏭️ push-zone: UNCALIBRATED — no owner-account list at $PUSH_ZONE_SRC, plain push allowed"
|
|
75
|
+
else
|
|
76
|
+
# One owner per line. Inline comments (`owner # note`), CR line endings, surrounding whitespace and a
|
|
77
|
+
# trailing slash are stripped BEFORE tokenizing — cross-family (codex gpt-5.5, 2026-09-05) showed an
|
|
78
|
+
# inline-comment word being read as an ALLOWED owner (fail-open) and a CRLF list blocking its own owner.
|
|
79
|
+
_pz_owners="$(sed -e 's/#.*$//' -e 's/\r$//' -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's#/*$##' "$PUSH_ZONE_SRC" 2>/dev/null | grep -v '^$' | tr '[:upper:]' '[:lower:]')"
|
|
80
|
+
_pz_url="${2:-}"
|
|
81
|
+
# git passes the destination's URL as $2. A bare remote NAME (no ':' and no '/') means the URL was
|
|
82
|
+
# not resolved for us (alias / insteadOf edge) — resolve it here rather than reading a name as a host
|
|
83
|
+
# and quietly landing in the "not github.com" allow branch (codex #5).
|
|
84
|
+
case "$_pz_url" in
|
|
85
|
+
*:*|*/*|"") ;;
|
|
86
|
+
*) _pz_url="$(git remote get-url --push "$_pz_url" 2>/dev/null || printf '%s' "$_pz_url")" ;;
|
|
87
|
+
esac
|
|
88
|
+
if [ -z "$_pz_url" ]; then
|
|
89
|
+
echo " ⏭️ push-zone: UNMEASURED — remote URL not supplied to the hook (\$2 empty), plain push allowed"
|
|
90
|
+
else
|
|
91
|
+
# Normalise before parsing: lowercase, trim, drop the scheme, drop userinfo (`user@`). The HOST is then
|
|
92
|
+
# everything up to the first ':' or '/'. This accepts every github.com shape git itself accepts —
|
|
93
|
+
# `https://user@github.com/o/r`, `https://github.com:443/o/r`, `git@github.com:o/r`, `git@github.com:/o/r`,
|
|
94
|
+
# `ssh://git@github.com:22/o/r` — instead of an allowlist of three literal prefixes that read the rest as
|
|
95
|
+
# "unrecognized → allow" (codex #1–#4: four real github.com shapes were failing OPEN).
|
|
96
|
+
_pz_url_lc="$(printf '%s' "$_pz_url" | tr '[:upper:]' '[:lower:]' | sed -e 's/[[:space:]]*$//' -e 's/^[[:space:]]*//')"
|
|
97
|
+
_pz_norm="$(printf '%s' "$_pz_url_lc" | sed -E 's#^[a-z][a-z0-9+.-]*://##; s#^[^@/]*@##')"
|
|
98
|
+
_pz_host="$(printf '%s' "$_pz_norm" | sed -E 's#[:/].*$##')"
|
|
99
|
+
if [ "$_pz_host" = "github.com" ]; then
|
|
100
|
+
_pz_path="$(printf '%s' "$_pz_norm" | sed -E 's#^github\.com(:[0-9]+)?[:/]+##')"
|
|
101
|
+
_pz_owner="${_pz_path%%/*}"
|
|
102
|
+
if [ -z "$_pz_owner" ] || [ "$_pz_path" = "$_pz_norm" ]; then
|
|
103
|
+
echo " ⏭️ push-zone: UNMEASURED — github.com remote in an unrecognized URL shape, plain push allowed"
|
|
104
|
+
else
|
|
105
|
+
_pz_hit=0
|
|
106
|
+
for _pz_o in $_pz_owners; do
|
|
107
|
+
[ "$_pz_owner" = "$_pz_o" ] && { _pz_hit=1; break; }
|
|
108
|
+
done
|
|
109
|
+
if [ "$_pz_hit" -eq 0 ]; then
|
|
110
|
+
echo ""
|
|
111
|
+
echo "══════════════════════════════════════════════"
|
|
112
|
+
echo " ⛔ FH Push-Zone Gate — remote is outside the owner-account list"
|
|
113
|
+
echo "══════════════════════════════════════════════"
|
|
114
|
+
echo " remote '$PUSH_REMOTE' → github.com/$_pz_owner"
|
|
115
|
+
echo " Plain git push is not this remote's channel — CLAUDE.local.md §REST API push"
|
|
116
|
+
echo " 계정규칙 (a non-owner account pushes via the REST Contents API instead)."
|
|
117
|
+
echo " Deliberate exception (explicit, logged): PUSH_ZONE_OK=1 git push …"
|
|
118
|
+
echo "══════════════════════════════════════════════"
|
|
119
|
+
if [ "${PUSH_ZONE_OK:-0}" = "1" ]; then
|
|
120
|
+
echo " ⚠️ FH Push-Zone: allowed by PUSH_ZONE_OK=1 (conscious, gated intent)"
|
|
121
|
+
mkdir -p "$REPO_ROOT/tracks/_meta" 2>/dev/null || true
|
|
122
|
+
printf '%s PUSH_ZONE_OK override — remote:%s owner:%s\n' \
|
|
123
|
+
"$(date +%Y-%m-%dT%H:%M:%S)" "$PUSH_REMOTE" "$_pz_owner" \
|
|
124
|
+
>> "$REPO_ROOT/tracks/_meta/.push_zone_override_log" 2>/dev/null || true
|
|
125
|
+
else
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
else
|
|
131
|
+
echo " ⏭️ push-zone: remote host is not github.com — outside this axis's scope, plain push allowed"
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
45
136
|
BASE="${FH_DESTRUCTIVE_BASE:-origin/main}"
|
|
46
137
|
# All ancestry/reachability checks below must ignore local `git replace`/graft objects — a graft can
|
|
47
138
|
# falsify merge-base/rev-list/ls-tree to make a divergent force look fast-forward or an unmerged branch
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Red-team report — ISO/IEC 42119-7 형 (템플릿, 2026-09-05)
|
|
2
|
+
|
|
3
|
+
> **쓰는 자리**: cross-family 적대 리뷰(`auto-decorrelation` Step 4~6) · `/steel-quench` Wave-1 · challenger 디스패치의 **산출을 담는 형식**.
|
|
4
|
+
> 새 검사가 아니라 **이미 하는 것의 보고 형식**이다 — 개발 중인 ISO/IEC AWI TS 42119-7(AI 레드티밍)의 **공개 스코프가 다루는 주제**
|
|
5
|
+
> (용어 · 위험 식별 · 적용 범위 · 목표/공격 벡터 · 계획/실행 방법 · 문서/보고 · 수명주기 통합 — 🟥 규범 «요구사항」은 미공개, 이건 «다루는 주제」다)
|
|
6
|
+
> 를 우리 산출이 이미 갖고 있으니 **이름표만 맞춘다**. §4 의 «처분」은 이 레포의 로컬 프로세스이지 표준 스코프 항목이 아니다.
|
|
7
|
+
> 정본 crosswalk: `knowledge/shared/harness-core/iso_ai_standards_crosswalk.md §2` (42119-7 행).
|
|
8
|
+
>
|
|
9
|
+
> 🟥 이 형식은 «잘 했다」를 증명하지 않는다. 채널이다 — 칸이 비어 있으면 안 돌린 것이고, 채워져 있어도 진위는 마커/PR 의 그라운딩이 진다.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
# RED-TEAM REPORT — <대상 한 줄> date: YYYY-MM-DD trigger: <4축 마커 트리거 / load-bearing gate / chamber / 요청>
|
|
13
|
+
|
|
14
|
+
## 0. 적용 범위와 용어 (applicability · terms)
|
|
15
|
+
대상 : <어느 AI 시스템/하네스의 어느 표면 — 코드·프롬프트·게이트·설정>
|
|
16
|
+
적용 범위 : <이 평가가 «보는」 것과 «안 보는」 것 한 줄씩>
|
|
17
|
+
용어 : <이 보고에서 S/A/B 가 뜻하는 것 · «fail-open」 등 국소 용어의 정의>
|
|
18
|
+
|
|
19
|
+
## 1. 위험 식별 (what could go wrong — 42119-7 "identification of risks")
|
|
20
|
+
표면 : <가역 / 비가역(publish·delete·history) / 소비자 가시>
|
|
21
|
+
실패 모드 : <이 변경이 틀렸다면 관측될 것 — 마커 defeater: 와 같은 문장>
|
|
22
|
+
AI 특성 : <비결정성 · 데이터 의존 · 적응성 · 불투명성 · 자율성 중 해당 — 없으면 «없음»>
|
|
23
|
+
|
|
24
|
+
## 2. 목표와 공격 벡터 (objectives & attack vectors)
|
|
25
|
+
목표 : S = <fail-open / 유출 / 차단 회피 … 이 대상에서 S 의 정의> A = <과차단 / 오탐 / …> B = 유지보수
|
|
26
|
+
공격 벡터 : <레지스트리 인용 — steel-quench Wave-1 각도 #n · 또는 프롬프트의 "attack specifically (1)…(n)" 목록 그대로>
|
|
27
|
+
|
|
28
|
+
## 3. 방법 (methodology — 29119 프로세스 정렬)
|
|
29
|
+
리뷰어 : <계열(codex/gemini/…) · 입장(cold / target-repo tier1b·tier2·tier2b·tier3) · 받은 것(diff만 / diff+주장 / 실행 출력)>
|
|
30
|
+
격리 : <리뷰어가 저자 추론을 봤나 — 안 봤다 / 봤다(사유)>
|
|
31
|
+
실행 : <리뷰어가 무엇을 실행했나 — 명령과 출력, 없으면 «정적»>
|
|
32
|
+
residency : <residency=CLEAN(files=n, stripped=m) — 스트립한 파일은 리뷰어에게 이름으로 알렸나>
|
|
33
|
+
|
|
34
|
+
## 4. 발견 (findings — 재현 입력을 반드시 함께) + 처분(로컬 프로세스)
|
|
35
|
+
| # | 심각도 | 위치(file:line) | 재현 입력 | 왜 | 처분(수리 / 반박 / 잔여) | 앵커(레인 id · fail-before) |
|
|
36
|
+
|---|---|---|---|---|---|---|
|
|
37
|
+
|
|
38
|
+
수리 n · 반박 n(각각 근거 한 줄) · 잔여 n(이름으로)
|
|
39
|
+
|
|
40
|
+
## 5. 수명주기 통합 (integration into the AI system life cycle)
|
|
41
|
+
어느 게이트에서 돌았나 : <pre-commit 4축 / Field-Harness Load-Bearing Change Gate / chamber SCREEN / 릴리스 전>
|
|
42
|
+
재실행 조건 : <이 표면이 다시 바뀌면 / 모델·계열이 바뀌면 / N 세션 뒤>
|
|
43
|
+
기록 위치 : <마커 crossfamily: · PR 본문 · fh_signal_*>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**채우는 규칙**
|
|
47
|
+
- §4 의 «재현 입력」 이 빈 발견은 발견이 아니라 의견이다 — 그 행은 처분 «반박」으로 가거나 지운다.
|
|
48
|
+
- «반박」은 근거 한 줄이 있어야 한다(실측이면 실측을, 계약이면 계약 문장을).
|
|
49
|
+
- 이 보고는 **마커를 대체하지 않는다**. 마커 `crossfamily:` 한 줄이 이 보고의 요약이고, 보고는 그 줄의 전개다.
|
|
@@ -71,7 +71,61 @@
|
|
|
71
71
|
"strings does not fire. Bash path (sed -i / > / tee into a .sh) covered with a weaker rule (named",
|
|
72
72
|
"residual in the script header). Evidence row in .claude/.proposal_hook_events.tsv. Relaying an",
|
|
73
73
|
"injected instruction is not initiative — see the r4 RESULT for what this does and does not claim.",
|
|
74
|
-
" bash scripts/test_proposal_hook_lanes.sh # expect: all known pairs hold (count self-reported)"
|
|
74
|
+
" bash scripts/test_proposal_hook_lanes.sh # expect: all known pairs hold (count self-reported)",
|
|
75
|
+
"",
|
|
76
|
+
"SIXTH GUARD, WebSearch|WebFetch matcher — outbound_query_hook.sh (added 2026-09-05):",
|
|
77
|
+
"the FIRST hook here that can emit permissionDecision:\"deny\", so read its degrade direction before",
|
|
78
|
+
"wiring it. WHAT IT GUARDS: a WebSearch/WebFetch query is an OUTBOUND, IRREVERSIBLE act — if the",
|
|
79
|
+
"string names an internal asset, the query IS the leak (CLAUDE.md §Field-Harness Diagnostic: internal",
|
|
80
|
+
"names never reach a log, a comment or a paste). scripts/outbound_query_guard.sh has owned that lint",
|
|
81
|
+
"since 2026-08-21 with ZERO callers, and its own header says so; this is its wiring. A repo scanner",
|
|
82
|
+
"cannot see an interactively-composed query, so the guard belongs on the call — same reasoning as",
|
|
83
|
+
"pipe_verdict_guard's.",
|
|
84
|
+
"",
|
|
85
|
+
"TWO LAYERS, AND ONLY ONE OF THEM DENIES. The CLI guard fail-closes when the operator's gitignored",
|
|
86
|
+
"override layer is absent. Correct for a CLI you type; wrong for a hook, because in a consumer",
|
|
87
|
+
"install that layer is absent BY CONSTRUCTION — importing that contract would deny every consumer's",
|
|
88
|
+
"WebSearch, and the bypass it would train is turning hooks off, disarming the destructive-op and",
|
|
89
|
+
"confidentiality guards riding the same channel. So:",
|
|
90
|
+
" override-layer hit -> deny (the operator's own internal literals)",
|
|
91
|
+
" defaults-layer hit -> advisory (universal shapes; FP-prone, and all a consumer has)",
|
|
92
|
+
" no override present -> advisory + a once-per-session UNCALIBRATED notice (a defaults-only scan",
|
|
93
|
+
" cannot say \"no internal tokens\", only \"no universal shapes\")",
|
|
94
|
+
" instrument incomplete-> deny (NOT SCANNED is not clean; irreversible surface fails CLOSED)",
|
|
95
|
+
" not an FH checkout -> silent (mechanical test: is the tracked defaults file here?)",
|
|
96
|
+
" hook command absent -> the tool RUNS. A PreToolUse command that cannot be found is a non-blocking",
|
|
97
|
+
" error, so on a consumer install where scripts/outbound_query_hook.sh is not at",
|
|
98
|
+
" $CLAUDE_PROJECT_DIR/scripts/ the deny you think you wired is fail-OPEN (cross-family",
|
|
99
|
+
" review, 2026-09-05). Same contract as the five guards above, but this one denies,",
|
|
100
|
+
" so VERIFY after wiring: echo '{}' | bash \"$CLAUDE_PROJECT_DIR/scripts/outbound_query_hook.sh\"; echo rc=$?",
|
|
101
|
+
" rc=0 and no output = resolved and silent on an empty payload; 'No such file' = not wired.",
|
|
102
|
+
"The layer is decided by comparing hit COUNTS across two scans (full set, then defaults-only). A",
|
|
103
|
+
"boolean would collapse \"both layers hit\" into \"defaults hit\" and silently downgrade a real deny.",
|
|
104
|
+
"",
|
|
105
|
+
"🟥 THE OUTPUT NEVER CARRIES THE TOKEN VALUE — only the count and the severity label. A leak guard",
|
|
106
|
+
"that echoes the token into its own deny reason or log is the leak channel. Lane H10 asserts the",
|
|
107
|
+
"fixture token appears on no output surface (stdout, stderr, event log).",
|
|
108
|
+
"",
|
|
109
|
+
"Opt out on one call by putting `# noqa: outbound` in the query. That channel is gameable, which is",
|
|
110
|
+
"exactly why it writes a log row — an invisible bypass is worse than a recorded one. Evidence rows in",
|
|
111
|
+
".claude/.outbound_hook_events.tsv: ts, tool, verdict(deny|advisory|clean|noqa|na), layer, and the",
|
|
112
|
+
"tool_input KEY NAMES (schema, never content) — the official hooks reference does not document",
|
|
113
|
+
"WebSearch/WebFetch tool_input keys, so the hook scans every string value rather than hardcoding a",
|
|
114
|
+
"key name that would fail SILENTLY OPEN, and logs the observed shape so it becomes measured.",
|
|
115
|
+
"",
|
|
116
|
+
"🟥 ADD THESE TWO TO YOUR .gitignore WHEN YOU WIRE THIS — the hook writes them on every matched",
|
|
117
|
+
"call and they are not tracked content:",
|
|
118
|
+
" .claude/.outbound_hook_events.tsv",
|
|
119
|
+
" .claude/.outbound_hook_uncalibrated_notice",
|
|
120
|
+
"Missed on the first build; a clean-clone run flagged both as tree contamination, which is also",
|
|
121
|
+
"what a consumer's `git status` would have shown after their first WebSearch.",
|
|
122
|
+
"",
|
|
123
|
+
"Verify after wiring (known pair, both directions):",
|
|
124
|
+
" bash scripts/test_outbound_query_hook_lanes.sh # expect: all known pairs hold (count self-reported)",
|
|
125
|
+
" printf '%s' '{\"tool_name\":\"WebSearch\",\"tool_input\":{\"query\":\"bash printf portability\"}}' \\",
|
|
126
|
+
" | bash scripts/outbound_query_hook.sh # expect: NO output (a clean query is silent)",
|
|
127
|
+
" bash scripts/test_outbound_query_lanes.sh # the CLI twin, for outbound paths this hook",
|
|
128
|
+
" # does NOT cover (curl / external CLIs via Bash)"
|
|
75
129
|
],
|
|
76
130
|
"project_settings_json": {
|
|
77
131
|
"hooks": {
|
|
@@ -115,6 +169,16 @@
|
|
|
115
169
|
"timeout": 5
|
|
116
170
|
}
|
|
117
171
|
]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"matcher": "WebSearch|WebFetch",
|
|
175
|
+
"hooks": [
|
|
176
|
+
{
|
|
177
|
+
"type": "command",
|
|
178
|
+
"command": "bash \"$CLAUDE_PROJECT_DIR/scripts/outbound_query_hook.sh\"",
|
|
179
|
+
"timeout": 5
|
|
180
|
+
}
|
|
181
|
+
]
|
|
118
182
|
}
|
|
119
183
|
]
|
|
120
184
|
}
|