Skip to content

fix(claude-config): permission-rule-check falls through to $PWD and sweeps the user profile, exiting 0 #2249

Description

@kyle-sexton

Defect

permission-rule-check.sh falls through to $PWD when it cannot resolve a repository root. Outside a
git repo that is whatever directory the session happens to stand in — on a developer machine, typically
the user profile — and the script then walks it unbounded with find, swallows every error, and exits
0. A timeout or a swallowed permission error is indistinguishable from a clean bill.

Evidence at HEAD

plugins/claude-config/skills/audit-permission-grants/scripts/permission-rule-check.sh:71-76:

if [[ -n "${PERMISSION_HYGIENE_FIXTURE_DIR:-}" ]]; then
  ROOT="$PERMISSION_HYGIENE_FIXTURE_DIR"
else
  ROOT="$(git rev-parse --show-toplevel 2>/dev/null | tr -d '\r')"
  [[ -n "$ROOT" ]] || ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
fi

Aggravators, all confirmed by grep at HEAD:

  • two unbounded find "$ROOT" walks with stderr discarded — :222 and :259;
  • no -maxdepth, no -prune list;
  • unconditional exit 0 at :264 (the --count path) and :272 (the report path);
  • disable-model-invocation: false at SKILL.md:5, so the skill is model-invocable.

There is no refusal branch for an unresolvable root.

Not reproduced, deliberately

The originating report measured a 5m00s timeout that did not complete. That measurement is carried
forward unverified.
Running the unbounded scan would sweep this machine's user profile, which the
operating instructions for this work forbid. The code path is provable from the repository without it;
only the magnitude rests on the unreproduced run.

What the fix must carry

  • Both output modes need the refusal. --count exits 0 at :264; a 0 printed from a scan that
    resolved no root is exactly the clean-bill confusion this is about.

  • Reuse exit 2 rather than minting a code. SKILL.md:56 already documents exit 2 as the
    environment-gap channel — "a missing jq exits 2 (report the environment gap rather than a clean
    bill)"
    — and an unresolvable scan root is precisely an environment gap reported instead of a clean
    bill. This keeps the advisory exit-0-for-findings contract intact.

  • The "always exits 0" statements must move together. grep at HEAD finds five, not the three
    the ledger recorded: reference/criteria.md:20, and scripts/permission-rule-check.sh:24, :43,
    :44, :50.

    Ledger correction: the ledger cites SKILL.md:56 as a third "always exits 0" site. It is not —
    grep -n "exits 0\|exit 0" over SKILL.md returns no match, and :56 is the jq-exits-2 line
    quoted above. SKILL.md carries no always-exits-0 claim to update.

Grading

  • Severity: HIGH — a model-invocable skill whose default failure mode is an unbounded filesystem
    walk that exits 0.
  • Provenance: AUDITOR_VERIFIED on the code path; the timing measurement is SELF_REPORTED and
    unreproduced.
  • Verdict: PRESENT_AT_HEAD.
  • Dedup: none found.
  • Originating inbox item: batch-4 ledger I10, row A4.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions