feat(conventions): codify hook-config-delivery channel matrix with CI gate - #1249
Conversation
… gate Adds the hook-config-delivery owner doc (docs/conventions/) — the channel decision matrix for delivering a plugin userConfig value into hook decision logic, version-pinned to CC 2.1.218 (docs re-fetched 2026-07-24; fresh-session probe 2026-07-23) with recheck triggers. Registers it in the PLUGIN-PHILOSOPHY convention registry. Channels A-F characterized, including the shipped direct-settings-read channel (F) from disk-hygiene 0.9.0 (#1242). Enforces the "never bare argv" rule with a new userconfig-argv-gate CI lane: scripts/check-hook-userconfig-argv.sh flags any ${user_config.*} token in a plugin hook config (default hooks/hooks.json, manifest-pointed files, inline manifest hooks objects; MCP/LSP configs out of scope), with a stale-guarded allowlist reserved for a proven channel D. Self-test-first job wired into the ci-status aggregate. Pins the exact regression #1242 fixed. Supersedes the draft matrix in #1182, which becomes the adoption pointer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 45s —— View job PR Review — feat(conventions): hook-config-delivery channel matrix
OverallSolid PR. The documentation is precise and unusually honest about what is doc-stated vs. probe-proven vs. inferred. The gate script is defensively written. The test suite is comprehensive and follows the broken-detector-cannot-mask pattern. No blocking issues — a few minor observations below.
|
|
Claude finished @kyle-sexton's task in 2m 45s —— View job Security Review — PR #1249Tasks
FindingsSUGGESTION · confidence: HIGH — Unvalidated path traversal in
|
| Area | Assessment |
|---|---|
| Workflow trigger | pull_request only — not pull_request_target / workflow_run; fork PRs are sandboxed |
| Workflow permissions | Global permissions: contents: read; new job adds no wider scope |
| Action pins | actions/checkout pinned to full commit SHA with inline tag comment; persist-credentials: false set |
${{ github context }} in run: |
New job has no env: or inline interpolation; no script-injection surface |
| Secrets | No secrets referenced anywhere in the new job |
grep/jq usage |
Fixed-string (-F) grep throughout; jq output only piped to grep or captured — no eval or shell expansion of untrusted data |
| Allowlist guard | grep -Fxq (whole-line, fixed-string) — no injection via allowlist content |
flag() echo |
No -e; no command expansion; line-number extracted by parameter expansion (${line%%:*}), not eval |
| Test script | Uses mktemp -d; all fixture content is hardcoded strings; run_check runs in a subshell |
| Documentation/convention docs | No executable content; no secrets |
No CRITICAL or IMPORTANT findings.
…w test gaps Review follow-ups on #1249: scan_manifest_path rejects absolute and ..-segment hooks paths from plugin.json with a visible skip (portable string check, no realpath dependency), so a crafted manifest cannot point the gate outside the tree it claims to scan. Adds three test scenarios (no-hooks-key manifest, out-of-tree path skipped visibly, composite default+manifest-pointed both flagged) — 16 total. README channel F now states why ${CLAUDE_PLUGIN_ROOT} is tamper-resistant inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Review + security findings triaged; fixes in 8d0d0aa.
Test suite now 16/16; shellcheck (repo rcfile) and markdownlint clean. |
|
Claude finished @kyle-sexton's task in 2m 27s —— View job Re-review — feat(conventions): hook-config-delivery channel matrix (post-8d0d0aa)Tasks
Verdict — all previous findings correctly resolvedThis is the follow-up pass after the triage table in kyle-sexton's comment. Each finding is verified below. 1. Path traversal →
|
|
Claude finished @kyle-sexton's task in 2m 53s —— View job Security Review — PR #1249 (re-review after 8d0d0aa)Tasks
Previous finding — resolvedThe path-traversal SUGGESTION from the first security review has been addressed in if [[ "$rel" == /* || "$rel" =~ ^[A-Za-z]: || "/$rel/" == *"/../"* ]]; then
Traced against the boundary cases relevant to the prior finding: New scan — no findings
No CRITICAL or IMPORTANT findings. No new SUGGESTION-level items. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8d0d0aa2a5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex P2 on #1249: a hook config can spell the token with a JSON unicode escape (backslash-u005f for the underscore) that the loader decodes before substitution, evading a raw-text grep. scan_file now adds a second pass — jq re-serializes the JSON, resolving escapes — and flags a file whose decoded content carries the token when the raw pass saw nothing. Raw pass keeps file:line precision; unparsable files keep the raw pass only. New test builds the escaped fixture via printf; 17 scenarios total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 2s —— View job Re-review — feat(conventions): hook-config-delivery matrix (post-f5fef73)Tasks
Codex finding — resolved ✓The fix at lines 82–93 correctly closes the hits="$(grep -nF "$TOKEN" "$file" || true)"
if [[ -n "$hits" ]]; then
# raw pass — precise file:line reporting
while IFS= read -r line; do flag "$file" "${line%%:*}"; done <<<"$hits"
elif jq -c . "$file" 2>/dev/null | grep -qF "$TOKEN"; then
# decoded pass — catches \u-encoded spellings the loader would resolve
flag "$file" "escaped token in decoded JSON"
fiWhy this is correct:
The decoded pass only runs when the raw pass returns zero hits. This means a file carrying both raw-text and escaped spellings would report only the raw hits. For the gate's goal (fail the PR), this is correct — the file is already flagged. The comment at line 77–81 documents this tradeoff explicitly ("raw pass is kept for file:line precision"). No issue. Allowlist interaction: An allowlisted file that carries only an escaped token goes through the decoded pass → Error message format: The decoded-pass branch reports Test scenario 13 —
|
| Surface | Assessment |
|---|---|
jq -c . re-serialization correctness |
$, {, . are not JSON-escapable; decoded token reproduced faithfully in output ✓ |
Multi-escape vectors (con…) |
jq decodes all Unicode escapes; combined decoding yields the full token ✓ |
| Unparsable file fallback | 2>/dev/null suppresses jq errors; raw-only pass used; consistent with prior *) handling ✓ |
| Allowlist × escaped-only file | flagged_or_allowed set by decoded pass; stale guard satisfied ✓ |
| 17/17 test count | All scenarios present in the file; no orphaned tests ✓ |
No blocking issues. No new concerns. The PR is ready to merge.
|
Claude finished @kyle-sexton's task in 4m 26s —— View job Security Review — PR #1249 (post-f5fef73)Tasks
Previous findings — status
f5fef73 — decoded-pass analysisThe commit adds a second scan pass to elif jq -c . "$file" 2>/dev/null | grep -qF "$TOKEN"; then
flag "$file" "escaped token in decoded JSON"
fiInjection surface: Scope of decoding: Interaction with Interaction with Inline object case (not via Test coverage for new path: Scenario 17 (lines 214–230) builds the Full surface scan — no findings
No CRITICAL, IMPORTANT, or SUGGESTION findings. |
Summary
Codifies the userConfig→hook channel decision matrix as a new
hook-*family owner doc,docs/conventions/hook-config-delivery/(README + CHANGELOG,contract_version1.0), registered inthe PLUGIN-PHILOSOPHY convention registry. It composes with
config-cascade(which ownsconsumer-tracked file layering; this owns the harness-prompted userConfig path), characterizes
channels A–F — including the direct-settings-read channel (F) that disk-hygiene 0.9.0 shipped in
#1242 — and version-pins every upstream fact to CC 2.1.218 with explicit recheck triggers
(docs re-fetched 2026-07-24; behavioral facts from the 2026-07-23 fresh-session probe).
Enforces the matrix's "never bare argv" rule with a new
userconfig-argv-gateCI lane:scripts/check-hook-userconfig-argv.shfails on any${user_config.*}token in a plugin hookconfig — the default
hooks/hooks.json, manifest-pointed hook files (string or array), and inlinemanifest
hooksobjects. MCP/LSP configs are out of scope (substitution there is sanctioned). Astale-guarded allowlist (
scripts/hook-userconfig-argv-allowlist.txt, currently comment-only) isreserved for a ratified channel D adoption once the G-required probe passes. This pins the exact
regression #1242 fixed: an unset-but-defaulted argv token silently drops the whole hook entry
(upstream
defaultunimplemented — anthropics/claude-code#46477, closed not-planned).Test plan
bash scripts/check-hook-userconfig-argv.test.sh— 13/13 scenarios green (bare token in default /manifest-pointed / array / inline configs fail with file:line; clean, MCP, unreferenced-sibling,
non-hooks-manifest cases stay quiet; allowlist honored; stale allowlist entries fail; comments
inert; unparsable manifest skipped). CRLF-tolerant on Windows (jq emits
\runder Git Bash).bash scripts/check-hook-userconfig-argv.sh— real tree passes (0.9.0 already removed the lastbare token).
shellcheck --rcfile=.shellcheckrcclean on both scripts;actionlint+ YAML parse clean onci.yml;markdownlint-cli2clean on the new/edited docs.ci-statusneeds aggregate.Related
No related issue: the delivery-channel program is tracked outside this tracker; this PR closes
nothing. Context: supersedes the draft matrix in #1182 (which stays open, demoted to the
adoption/tracking pointer), builds on #1242 (disk-hygiene 0.9.0, closed #1019).
🤖 Generated with Claude Code