fix(hooks): deny non-string tool_name in check-pr-duplicate-issue.sh (#1315) - #1363
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
jq -r '.tool_name // empty' never errors on a non-string tool_name -- it silently falls through as "not our tool" (exit 0) instead of failing closed, the same type-confusion class PR #1213/#1217 already closed in six sibling hooks. Adds the identical, already-proven (.tool_name == null) or (.tool_name | type == "string") guard from check-pr-issue-acm-disclosure.sh, a regression test, and a GuardedField registration in the shared jq type-confusion matrix. Refs #1315
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1363 +/- ##
========================================
Coverage 99.54% 99.55%
========================================
Files 123 124 +1
Lines 21912 22019 +107
Branches 2682 2699 +17
========================================
+ Hits 21813 21920 +107
Misses 99 99 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The mandatory refactor/simplify and independent adversarial review passes (issue #1315) found no correctness or security defects; applied the refactor pass findings: trimmed a comment restating logic the preceding block already explains, removed stale hook-count claims from the shared matrix file (now a seventh hook), collapsed a redundant per-entry hermeticity explanation, and reworded an inline comment to match sibling hooks issue/PR-citation convention rather than naming review provenance directly. Refs #1315
|
The
This will fail identically for any PR/commit in this repository until either the missing script is added or the (external) workflow definition is corrected -- out of scope for issue #1315's own Generated by Claude Code |
… review An independent adversarial review (drafting-a-pr-to-merge step 8, issue #1315) found the fix itself correct with no runtime defect, but two registry/documentation consistency gaps: - .gitapex/ssot.json's pr-duplicate-issue gate rule text was not updated for the new tool_name-type guard, unlike the six sibling hooks that already carry this clause (matching the wording those hooks use). - The jq type-confusion matrix file claims to be the "current, authoritative registry" for every guarded field carrying this shape, but omitted this hook's own existing tool_input guard. Registered it too, per issue #1315's own residual-risk note naming this an allowed completeness addition -- it passes green immediately, since the underlying guard was already correct. Refs #1315
Summary
hooks/check-pr-duplicate-issue.shdenies (exit 2) instead ofsilently allowing (exit 0) when
tool_namein the PreToolUse payload isa non-string JSON value (array/object/number/bool) -- the same
type-confusion class PR #1213/#1217 already closed in six sibling hooks.
Facts
mainbefore this fix: an array-wrappedtool_namelet acreate_pull_requestcall skip this hook's ownopen-PR-duplicate-citation check entirely (a payload with a
resolving-style citation in its
bodyfield, piped intohooks/check-pr-duplicate-issue.sh, exited0instead of the expected2-- see issue hooks/check-pr-duplicate-issue.sh: fails open on a non-string tool_name (same class as #1208, #1217) #1315's own Facts section for the exact reproductioncommand).
tool_input-shape guard at line 99 is already present andunaffected; only the
tool_name-shape guard was missing.hooks/check-pr-issue-acm-disclosure.sh's ownalready-proven
(.tool_name == null) or (.tool_name | type == "string")guard (PR fix(hooks): fail closed on missing/malformed jq in four PreToolUse gates #1213/hooks/: check-pr-issue-acm-disclosure.sh and check-pr-title-convention.sh fail open on a non-string tool_name (same class as #1208) #1217), verbatim.Assumptions
None beyond the issue's own stated scope.
Risk / blast radius
Low. Purely additive fail-closed guard on one PreToolUse hook; no
existing allow-path behavior changes (only a previously-silent-allow path
for a malformed
tool_namenow denies).Rollback
git revertthis PR's merge commit; the hook reverts to its prior(vulnerable) behavior.
Acceptance Criteria Map
(from issue #1315, re-verified unchanged this session -- see the
re-verification marker on the issue's own body)
tool_namemust deny (exit 2), not silently fall through as "not our tool"tool_nameof the wrong JSON type the same way PR #1213 made four other hooks do, and PR #1314 madecheck-pr-issue-acm-disclosure.sh/check-pr-title-convention.shdo(.tool_name == null) or (.tool_name | type == "string")immediately before this hook's owntool_name=$(...)extraction (line 84)printf '%s' '{"tool_name":["X"],...}' | bash hooks/check-pr-duplicate-issue.sh; echo $?returns 2, not 0; a new pytest case asserts this directlyhooks/test_gitapex_check_pr_duplicate_issue_shell.py) covered no non-stringtool_namecasetest_denied_when_tool_name_is_not_a_stringand PR #1314's equivalent additions to the two origin hooksuv run --frozen pytest hooks/test_gitapex_check_pr_duplicate_issue_shell.py -vpasses with the new cases includedhooks/test_gitapex_jq_type_confusion_matrix.py, issue #1312) should also cover this hookGuardedFieldentry forcheck-pr-duplicate-issue.sh'stool_namefield toGUARDED_FIELDSuv run --frozen pytest hooks/test_gitapex_jq_type_confusion_matrix.py -qpasses with the new entry includedVerification
All three rows above proved, post-implementation (commit
b9729820;step-8 review rounds landed comment polish in
3a33863band tworegistry-consistency fixes in
452a94fa, no behavior change to theguard itself):
printf '%s' '{"tool_name":["mcp__github__create_pull_request"],"tool_input":{"owner":"tvna","repo":"gitapex","title":"t","body":"Refs #1"}}' | GH_TOKEN= GITHUB_TOKEN= bash hooks/check-pr-duplicate-issue.sh; echo $?->2(deny), was0pre-fix.uv run --frozen pytest hooks/test_gitapex_check_pr_duplicate_issue_shell.py -v-> 24 passed (newtest_denied_when_tool_name_is_not_a_string[array/object/number/bool]cases confirmed Red pre-fix, Green post-fix).uv run --frozen pytest hooks/test_gitapex_jq_type_confusion_matrix.py -q-> 135 passed (newpr-duplicate-issue:tool_nameandpr-duplicate-issue:tool_inputentries included).uv run --frozen pytest hooks -q-> 533 passed, no regressions (confirmed independently in both step-8 review rounds).ruff check,ruff format --check,mypy --strictall pass on every touched Python file;bash -nconfirms the modified hook's shell syntax;.github/scripts/gitapex_scan_ssot_schema.pyreports no ssot.json drift.Skill audit evidence
deterministic gate (
hooks/check-pr-duplicate-issue.sh, pergitapex_detect_changed_gate_scripts.py). Read againstskills/evaluating-deterministic-gate-quality/references/dimensions.mddimension 15 (fail-closed on malformed input) specifically: the new
guard is the exact, already-proven
(.tool_name == null) or (.tool_name | type == "string")predicate from PR fix(hooks): fail closed on missing/malformed jq in four PreToolUse gates #1213/hooks/: check-pr-issue-acm-disclosure.sh and check-pr-title-convention.sh fail open on a non-string tool_name (same class as #1208) #1217 (no new detectionlogic invented), and the new hook-local pytest case plus the shared
hooks/test_gitapex_jq_type_confusion_matrix.pyregistration bothconfirm it fails closed (exit 2) for every non-string/non-null matrix
value (array/object/number/bool/zero/empty-string), not only the
happy-path string case.
test_denied_when_tool_name_is_not_a_stringparametrized test (array/object/number/bool) was written test-first: it
fails against the pre-fix hook (confirmed live, all 4 cases red before
the guard was added) and passes only once the guard lands, specifically
targeting the new detection logic's boundary (a non-string, non-null
tool_name) rather than only exercising its happy path. Two independentstep-8 review rounds additionally mutation-tested the guard directly
(temporarily disabled it, confirmed the tests fail for the right
reason, restored it) and live-tested the full 8-value jq
falsy/type-confusion matrix plus adjacent hostile inputs against the
shipped guard; no fail-open found.
Checklist
self-documenting via its own inline comments, no separate doc)
skills/*/SKILL.md, adocs/superpowers/specs/*.mddesign doc, a security-relevantskill, or a deterministic checker script
(
skills/*/scripts/*.py,evals/scripts/*.py,.github/scripts/*.py), a## Skill audit evidencesectiondiscloses the required verdicts/waivers (this PR modifies a
registered deterministic gate,
hooks/check-pr-duplicate-issue.sh-- see
## Skill audit evidenceabove)evals/*/split.md(n/a)
skills/*/SKILL.md's Stop-boundarybullets or named dispatch branches (n/a)
Merge gate: independent review
This PR is also subject to the
independent-review-pendingrequiredstatus check (see
.github/workflows/independent-review-pending.yml/.github/scripts/gitapex_gate_independent_review_pending.py). See the## Independent review verdictsection below.Independent review verdict
Outer layer: GitHub Copilot review requested via
request_copilot_review(comment-only -- Copilot's review carries no pass/fail signal of its own,
a materially weaker guarantee than Anthropic's Claude Code Review App
would give; that App is not confirmed installed/configured for this
repository, so this is disclosed rather than treated as equivalent).
Inner layer: two independent fresh-context adversarial-review dispatches
ran against this diff (single dispatch per round -- the diff is small
enough that category fan-out was not needed), each explicitly framed as
holding no assumption the diff is correct:
(
.gitapex/ssot.json's gate rule text not updated for the new guard;the shared jq type-confusion matrix's own "authoritative registry"
claim left this hook's existing
tool_inputguard unregistered). Bothindependently verified against the actual code, fixed, and re-tested
green (commit
452a94fa).452a94fa): CLEAN. Livemutation-tested both the new
tool_nameguard and the newly-registeredtool_inputmatrix entry (each guard temporarily disabled, confirmedthe corresponding tests fail for the right reason, then restored and
re-confirmed green); ran the full
hooks/suite (533 passed),ruff,ruff format --check,mypy, and.github/scripts/gitapex_scan_ssot_schema.py(no drift). No finding survived independent verification at or above
the 0.7 confidence bar.
Execution log
PlanApproved{run_id: 333ae4b9}TaskStarted{run_id: 333ae4b9, task_id: task-1}TaskCompleted{run_id: 333ae4b9, task_id: task-1, commit_sha: b9729820}TaskCompleted{run_id: 333ae4b9, task_id: step-8-refactor-review, commit_sha: 3a33863b}TaskCompleted{run_id: 333ae4b9, task_id: step-8-review-fixes, commit_sha: 452a94fa}Related Issue
Closes #1315