Skip to content

Audit and harden Claude Code commands: retire 3 stale, purge dead GitHub MCP for gh CLI, add argv-safe helpers#711

Merged
igerber merged 6 commits into
mainfrom
skill-audit
Jul 20, 2026
Merged

Audit and harden Claude Code commands: retire 3 stale, purge dead GitHub MCP for gh CLI, add argv-safe helpers#711
igerber merged 6 commits into
mainfrom
skill-audit

Conversation

@igerber

@igerber igerber commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Audit and hardening of the project's Claude Code commands (they are commands, not skills). Retires three that were stale/unused and hardens the PR / worktree / pre-merge workflow. Developer tooling only — no library or methodology changes.
  • Retired three commands:
    • review-pr — 10 lifetime uses, dead MCP calls, posted publicly with no confirmation, severity scale incompatible with the rest; /ai-review-local (pre-PR) and CI codex (post-PR) cover the space.
    • validate-tutorial — its core commands invoked jupyter nbconvert, which is not (and never was) installed; folded into pre-merge-check as an nbmake check.
    • dev-checklists — no unique content and pointed at a TODO.md section that no longer exists; the load-bearing rules moved to CONTRIBUTING.md.
  • Purged the dead GitHub MCP path. No MCP server is configured in this environment, so every mcp__github__* call hit an empty tool namespace. submit-pr / push-pr-update now use the gh CLI, and the redundant /ai-review comment post (which double-fires the auto reviewer) is removed.
  • Closed shell-injection structurally with two tested helpers, instead of interpolating untrusted values into command prose:
    • .claude/scripts/pr_prepare.py — PR title/branch/base arrive as files (written with the Write tool, never a shell) and reach git/gh only as quoted, validated variables; commit messages use git commit --file, not heredocs.
    • .claude/scripts/premerge_scan.py — discovers changed files via git … -z argv arrays, runs the methodology pattern checks (A–D) as pure-Python regex/AST over file content (opening a path never executes a filename), screens metacharacter paths, fails closed on git errors, is status-aware (added/modified/deleted, untracked), and emits NUL-delimited run-lists consumed portably via xargs -0 … < list.
  • submit-pr is now idempotent — its goal is "ensure this committed branch is pushed and has an open PR," so a committed, already-pushed branch opens its PR rather than dead-ending at "nothing to submit" (the standard flow commits at /ai-review-local, so submit-pr always runs on already-committed work).
  • worktree-new no longer silently branches from a non-default HEAD (it asks first), resolves the default branch via git ls-remote --symref (not gh repo view <alias>), and pins VIRTUAL_ENV for maturin develop so a rebuild lands in the worktree's own venv.
  • Contract tests (tests/test_command_contract.py) enforce these safety properties against the command prose, so a later edit can't silently reintroduce filename/value interpolation or a heredoc commit.

Deferred (tracked in TODO.md): worktree-rm's destructive-logic rewrite (confirmation gate, detached-HEAD rescue) was reverted to its main version because editing that prose repeatedly reintroduced shell-injection; restoring those guards wants a from-scratch worktree_rm.py helper. Also tracked: a staged-blob refinement for premerge_scan.py, and re-adding committed-range scanning to push-pr-update via the helper's --range mode.

Methodology references (required if estimator / math changes)

  • Method name(s): N/A - no methodology changes (developer tooling only)
  • Paper / source link(s): N/A
  • Any intentional deviations from the source (and why): None

Validation

  • Tests added/updated: tests/test_pr_prepare.py, tests/test_premerge_scan.py, tests/test_command_contract.py, tests/test_commit_message_safety.py — including injection regression tests that place $(touch sentinel) payloads in PR titles, commit messages, and staged + untracked filenames and assert nothing executes. All green; ruff / black / mypy clean on the helpers.
  • Backtest / simulation / notebook evidence (if applicable): N/A

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

igerber added 2 commits July 19, 2026 19:50
…gv-safe helpers

Ships the injection-safe, tested subset of the command audit. Two helpers
route every untrusted value through argv-safe Python; the destructive
worktree-rm rewrite was reverted to main after its prose kept reintroducing
shell-injection (see "Reverted" below).

MCP is dead, not merely disfavored. No GitHub MCP server is configured
(only playwright), so every mcp__github__* call hit an empty tool
namespace. Replaced with the `gh` CLI in submit-pr and push-pr-update, and
the /ai-review comment post (which double-fires the auto reviewer) removed.

Shell-injection closed structurally with two tested helpers:
- .claude/scripts/pr_prepare.py — resolves/validates the PR title, branch,
  and base. Untrusted values arrive as files (written by the Write tool,
  never a shell) and reach git/gh only as quoted, validated variables.
  Commit messages use `git commit --file`, not a heredoc.
- .claude/scripts/premerge_scan.py — discovers changed files via
  `git … -z` argv arrays, runs the methodology pattern checks (A–D) as
  pure-Python regex/AST over file *content* (opening a path never executes
  a filename), resolves test coverage, screens metacharacter paths, fails
  closed on git errors, and emits NUL-delimited run-lists consumed via
  `xargs -0 … < list`. Status-aware (added/modified/deleted, untracked).
pre-merge-check, submit-pr, and push-pr-update call the helpers instead of
greping filenames. Regression tests place `$(touch sentinel)` payloads in
titles, commit messages, and staged+untracked filenames and assert nothing
executes.

Retired three commands:
- review-pr: 10 lifetime uses, dead MCP calls, posted publicly with no
  confirmation, incompatible severity scale. /ai-review-local + CI codex
  cover the space.
- validate-tutorial: invoked `jupyter nbconvert`, never installed. Folded
  into pre-merge-check as an nbmake check.
- dev-checklists: no unique content; pointed at a TODO.md section that no
  longer exists. The load-bearing rules (np.errstate, fix-all-sites,
  parameter downstream-propagation) moved to CONTRIBUTING.md.

worktree-new: no longer silently branches from current HEAD when it is not
the default branch (asks first); resolves the default branch via
`git ls-remote --symref` (not `gh repo view <alias>`); and pins VIRTUAL_ENV
for `maturin develop` so a rebuild lands in the worktree's own venv.

Reverted (regressing prose, kept out of this PR):
- worktree-rm.md → main. The destructive-logic rewrite (confirmation gate,
  detached-HEAD rescue, state persistence) repeatedly reintroduced
  shell-injection — most recently by sourcing a state file built from a
  git-derived branch name. Reverted to the known main version; a from-scratch
  worktree-rm helper is the right way to land those safety improvements.
- push-pr-update committed-range scan removed rather than interpolate a raw
  comparison ref into a shell command; committed-change review is deferred
  to /pre-merge-check.

Commit and PR bodies carry no authorship trailers.
…e branch is already committed and pushed

The revised submit-pr keyed its exit on 'is there work to push', but the
standard flow commits at /ai-review-local (which requires a commit) and may
push during a rebase/prep step — so a committed, already-pushed branch hit
'nothing to submit' and never opened its PR. Reframe the contract to
'ensure this committed branch is pushed and has an open PR', idempotent:
push only when there are unpushed commits or no upstream, then open a PR
only if 'gh pr view' shows none. Uncommitted changes (which never went
through /ai-review-local) warn and offer commit-or-abort rather than being
silently absorbed.
@github-actions

Copy link
Copy Markdown

Overall Assessment

⚠️ Needs changes — two unmitigated P1 workflow defects remain.

Executive Summary

  • No estimator, inference, weighting, variance, or identification methodology changed.
  • File/title shell-injection hardening is generally sound.
  • Clean zero-diff branches can still be created and pushed before PR creation fails.
  • Existing PR detection silently ignores requested base/draft state.
  • Three deferred limitations are properly tracked in TODO.md.

Methodology

No findings. No registered causal method is affected, so paper-level cross-checking is not applicable.

Code Quality

1. Clean zero-diff state creates and pushes a useless branch

  • Severity: P1
  • Location: .claude/commands/submit-pr.md:L148-L170, L301-L325
  • Impact: On a clean base branch with no commits ahead, the command creates and pushes a new branch, then gh pr create fails because there is no diff. This violates the new idempotent contract and leaves remote branch clutter.
  • Concrete fix: Before branch creation or push, compare HEAD with $BASE_REMOTE/$BASE_BRANCH. If no matching PR exists and the clean branch has zero commits ahead, exit with “nothing to submit.” Add a regression test for clean main == origin/main.

2. Any open PR satisfies the idempotency check

  • Severity: P1
  • Location: .claude/commands/submit-pr.md:L373-L417
  • Impact: gh pr view checks only url,state. An open PR targeting another base is reported as success even when the user supplied a different --base; --draft can likewise be silently ignored.
  • Concrete fix: Query and compare headRefName, baseRefName, and isDraft, using the target repository explicitly for fork workflows. Only treat an exact match as success; otherwise update, create the requested PR, or report the mismatch.

3. Failed scans can leave stale run lists

  • Severity: P2
  • Location: .claude/scripts/premerge_scan.py:L257-L270, L298-L344; .claude/commands/pre-merge-check.md:L58-L70
  • Impact: Exit code 4 occurs before run-list replacement, while the deterministic scratch directory may retain lists from a prior run. Continuing could execute unrelated tests and misstate coverage.
  • Concrete fix: Remove or truncate both run lists at startup, write temporary files, and atomically replace them only on success. Explicitly require callers to stop on exit 4.

Performance

No findings.

Maintainability

No additional findings.

Tech Debt

Tracked limitations

  • Severity: P3 — informational
  • Location: TODO.md:L50-L52
  • Impact: worktree-rm hardening, staged-blob scanning, and committed-range scanning remain deferred.
  • Concrete fix: None required for this PR; all three are properly tracked.

Security

No unmitigated findings. File-based ingress, argv-based subprocess calls, quoting, and git commit --file address the introduced injection surfaces appropriately.

Documentation/Tests

Missing state-machine regression coverage

  • Severity: P2
  • Location: tests/test_command_contract.py:L69-L103
  • Impact: Tests cover textual injection contracts but not the zero-diff or mismatched-existing-PR states above.
  • Concrete fix: Add mocked git/gh workflow tests for both states.

Path to Approval

  1. Add the clean zero-ahead guard before creating or pushing a branch, with a regression test.
  2. Make existing-PR detection validate the requested head, base, and draft state, with direct and fork workflow tests.

…ogic)

- submit-pr zero-diff guard (P1): the idempotent reframe removed the 'nothing
  to submit' exit entirely, so a clean branch with zero commits ahead of base
  would create+push a useless branch and then fail gh pr create. Restore the
  exit, scoped to 'zero commits ahead of $BASE_REMOTE/$BASE_BRANCH' (not the
  old 'no unpushed commits', which wrongly caught committed-but-pushed work).
- submit-pr PR-match idempotency (P1): the existing-PR check read only url/state,
  so any open PR counted as success even with a different --base/--draft. Now
  compares baseRefName/isDraft and reports a mismatch instead of false success.
- premerge_scan run-lists (P2): truncate run-tests.z/run-notebooks.z at startup
  so an early exit (3/4) cannot leave a prior run's stale lists in the reused
  deterministic scratch dir. Regression test asserts a seeded stale list is
  cleared on a git-error exit.
- Contract tests assert the zero-diff guard and base/draft comparison are present.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 08ae8c26276f6e904f54930adb681eaa899bb2e3


Overall Assessment

Looks good — both prior P1 findings are resolved; no unmitigated P0/P1 remains.

Executive Summary

  • Zero-diff branches are now rejected before branch creation or push.
  • Existing PRs are checked for requested base and draft state.
  • No estimator, inference, variance, weighting, or identification methodology changed.
  • Stale scan lists are cleared before failure paths.
  • Remaining findings are non-blocking P2/P3 items.

Methodology

No findings. The diff does not affect any method registered in docs/methodology/REGISTRY.md; paper-level comparison is not applicable.

Code Quality

Scan failure is not explicitly terminal

  • Severity: P2
  • Location: .claude/commands/pre-merge-check.md:L58-L67, .claude/commands/submit-pr.md:L213-L230, .claude/commands/push-pr-update.md:L169-L186
  • Impact: premerge_scan.py exits 4 on git/read failures, but callers only document exit 3 and findings. A command could continue after an incomplete scan with empty run lists.
  • Concrete fix: Explicitly require all three callers to stop and report the error on exit 4.

Performance

No findings.

Maintainability

Control-group review guidance was lost

  • Severity: P2
  • Location: .claude/commands/review-plan.md:L237-L254, CONTRIBUTING.md:L149-L188
  • Impact: Deleting dev-checklists.md removed the explicit requirement to verify control/comparison-group composition for new estimator paths.
  • Concrete fix: Add that check to review-plan.md or the new Implementation Guidelines.

Tech Debt

Tracked limitations

  • Severity: P3 — informational
  • Location: TODO.md:L50-L52
  • Impact: worktree-rm hardening, staged-blob scanning, and committed-range scanning remain deferred.
  • Concrete fix: None required; all are properly tracked.

Security

No findings. File-based untrusted input, argv-based subprocesses, and git commit --file adequately protect the newly changed surfaces.

Documentation/Tests

Existing-PR contract test is too permissive

  • Severity: P2
  • Location: tests/test_command_contract.py:L95-L103
  • Impact: The test only searches the entire document for baseRefName and isDraft; it could pass if those fields were removed from the executable gh pr view query but remained in prose.
  • Concrete fix: Inspect the fenced command and assert both fields are present in its --json argument, or add a mocked workflow test.

Validation note: helper syntax and pure-function smoke checks passed. The focused pytest suite could not run because pytest is not installed.

- Document premerge_scan exit 4 (git/read failure = incomplete scan) in all
  three callers (pre-merge-check, submit-pr, push-pr-update) with an explicit
  'stop and report, do not continue on an empty scan' instruction.
- Restore the control/comparison-group composition check (lost with
  dev-checklists deletion) to CONTRIBUTING.md Implementation Guidelines:
  verify the comparison group on new code paths, not just the default.
- Tighten the existing-PR contract test to assert baseRefName/isDraft are on
  the executable 'gh pr view --json' line, not merely somewhere in the prose.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 144c2c166f5787804cf0b32fff0217cacd21b68c


Overall Assessment

Blocker — one unmitigated P0 security vulnerability remains.

Executive Summary

  • [Newly identified] Repository-derived refs can still be interpolated into shell source.
  • Git accepts branch names containing backticks and $(), enabling local command execution.
  • All previous review findings are resolved.
  • No estimator, inference, weighting, variance, or identification methodology changed.
  • Deferred limitations are properly tracked in TODO.md.

Methodology

No findings. No registered estimator or statistical behavior changed, so paper-level comparison is not applicable.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings. The prior control-group guidance finding is resolved in CONTRIBUTING.md:L175-L183.

Tech Debt

Tracked limitations

  • Severity: P3 — informational
  • Location: TODO.md:L50-L52
  • Impact: Worktree removal hardening, staged-blob scanning, and committed-range scanning remain deferred.
  • Concrete fix: None required; all are tracked.

Security

[Newly identified] Repository refs still reach shell source unsafely

  • Severity: P0
  • Location: .claude/commands/push-pr-update.md:L84-L120
  • Impact: <default-branch> and <comparison-ref> are substituted unquoted into git fetch, rev-list, and git diff. Git accepts names such as x$(touch sentinel) and x`id` , so following this prose can execute repository-controlled text under the developer’s account.
  • Concrete fix: Capture refs as data—preferably in an argv-based helper—or derive them into shell variables through command substitution and use only quoted forms such as "$COMPARISON_REF..HEAD". Remove every raw placeholder from executable command text.

Documentation/Tests

No separate findings. The prior existing-PR contract-test weakness is resolved at tests/test_command_contract.py:L101-L113.

Validation note: Python syntax and pure-helper smoke checks passed. The focused pytest suite could not run because pytest is unavailable.

Path to Approval

  1. Replace all raw <default-branch> and <comparison-ref> command interpolation in /push-pr-update with an argv-safe or quoted-variable flow.
  2. Add a regression test using a ref containing $(touch sentinel) and assert the sentinel is never created.

…ng placeholders (CI P0)

Pre-existing (17 sites on main): <comparison-ref>/<default-branch> were pasted
unquoted into git fetch/rev-list/diff. git accepts $()/backticks in ref names,
so a hostile default-branch/ref could execute. Resolve DEFAULT_BRANCH and
COMPARISON_REF into shell variables (deterministic; re-derived per block since
variables don't persist across tool calls) and use only quoted "$COMPARISON_REF..HEAD"
forms. Contract test asserts no raw <comparison-ref>/<default-branch> in an
executable bash line. Verified the resolution block runs clean under zsh.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 342ac04e7f4f0eef841d5d2355355996e9602fe6


Overall Assessment

Looks good — no unmitigated P0 or P1 findings.

Executive Summary

  • The previous P0 ref-injection vulnerability is resolved.
  • All repository-derived refs are captured as data and used quoted.
  • No estimator, inference, variance, or methodology code changed.
  • Deferred limitations are tracked in TODO.md.
  • One non-blocking security regression-test gap remains.

Methodology

No findings. The PR does not change statistical behavior, so Registry or paper comparison is not applicable.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

Tracked limitations

  • Severity: P3 — informational
  • Location: TODO.md:L50-L52
  • Impact: Worktree removal hardening, staged-blob scanning, and committed-range scanning remain deferred.
  • Concrete fix: None required for approval; each limitation is explicitly tracked.

Security

No findings. The prior P0 is fixed in .claude/commands/push-pr-update.md:L47-L130: DEFAULT_BRANCH and COMPARISON_REF are resolved into variables and every executable use is quoted.

Documentation/Tests

Hostile-ref regression test is only textual

  • Severity: P2
  • Location: tests/test_command_contract.py:L202-L212
  • Impact: The test rejects raw placeholders but would not catch an unquoted use such as $COMPARISON_REF..HEAD; it does not execute a ref containing backticks or $() as previously requested.
  • Concrete fix: Add a subprocess regression using a metacharacter-bearing ref and sentinel, and assert all variable occurrences remain quoted.

Validation note: both helper scripts parse successfully. The focused pytest, Ruff, and mypy checks could not run because those tools are unavailable in the review environment.

The prior contract test only rejected the raw <placeholder>; a bare unquoted
$COMPARISON_REF..HEAD would still be injectable. Add (1) a quoting assertion —
every $COMPARISON_REF/$DEFAULT_BRANCH use in a bash block must be quoted, and
(2) a subprocess regression proving a ref value holding $(...) as data is inert
when used through the command's quoted "$REF..HEAD" form.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: ac415a0f1a2a8ad151c958b2598411a56e14f937


Overall Assessment

Looks good — no unmitigated P0 or P1 findings.

Executive Summary

  • The previous hostile-ref regression-test gap is resolved.
  • All current ref-variable uses remain quoted; no raw ref placeholders occur in executable blocks.
  • No estimator, inference, variance, assumptions, or defaults changed.
  • Remaining limitations are explicitly tracked in TODO.md.

Methodology

No findings. No statistical methods are affected, so paper or Registry comparison is not applicable.

Code Quality

No findings. Both new helper scripts parse successfully.

Performance

No findings.

Maintainability

No findings. Shared scanning and PR preparation logic is centralized in tested helpers.

Tech Debt

Tracked command limitations

  • Severity: P3 — informational
  • Location: TODO.md:L50-L52
  • Impact: Worktree removal hardening, staged-blob scanning, and committed-range methodology scanning remain deferred.
  • Concrete fix: None required for approval; follow the tracked backlog rows.

Security

No findings. Ref resolution and use remain safe in .claude/commands/push-pr-update.md:L94-L130.

Documentation/Tests

No findings. The previous P2 is addressed by the quote contract and sentinel subprocess regression in tests/test_command_contract.py:L202-L240.

Validation note: pytest, Ruff, and mypy were unavailable; static checks found nine fenced ref-variable uses, all quoted, and zero raw executable ref placeholders.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Jul 20, 2026
@igerber
igerber merged commit aa429cb into main Jul 20, 2026
29 of 30 checks passed
@igerber
igerber deleted the skill-audit branch July 20, 2026 12:02
@igerber
igerber restored the skill-audit branch July 20, 2026 19:00
@igerber
igerber deleted the skill-audit branch July 20, 2026 19:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant