Skip to content

feat(integrity-guard): user-configurable path exclusion list#175

Open
Fearvox wants to merge 1 commit into
tanweai:mainfrom
Fearvox:feature/integrity-guard-configurable-exclusions
Open

feat(integrity-guard): user-configurable path exclusion list#175
Fearvox wants to merge 1 commit into
tanweai:mainfrom
Fearvox:feature/integrity-guard-configurable-exclusions

Conversation

@Fearvox
Copy link
Copy Markdown

@Fearvox Fearvox commented May 23, 2026

Problem

hooks/integrity-guard.sh fires an advisory on any Write / Edit / Bash mutating command that touches a CLAUDE.md, memory/ path, or .claude/settings.json — anywhere on the filesystem. This is correct for canonical project governance files, but is noisy for derived / projected directories (symlink views, build caches, mirrors, generated docs) where the same-named files are governance pointers to upstream, not canonical persistent memory themselves.

Concrete reproducer: a user maintains ~/projects/myrepo/views/admin/CLAUDE.md that is a guardrail pointer to ~/projects/myrepo/04-memory/. Every touch / Write / Edit on the view's CLAUDE.md fires advisory, even though the write is pre-authorized and the file isn't itself canonical memory.

PUA_INTEGRITY_FORCE=1 PUA_CONFIG=/nonexistent bash hooks/integrity-guard.sh <<'EOF2'
{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"touch /repo/views/admin/CLAUDE.md"}}
EOF2

Output:

{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"PUA Integrity Guard (advisory): Persistent-memory risk: ... Target: /CLAUDE.md"}}

There is currently no way to suppress this without disabling Integrity Guard wholesale (always_on: false) or editing the cached script directly (lost on next plugin upgrade).

Fix

Read ~/.pua/integrity-guard-exclusions.json (overridable via \$PUA_INTEGRITY_EXCLUSIONS). Format:

```json
{"patterns": ["(^|/)derived-view-[^/]+/CLAUDE\\.md$"]}
```

Bare JSON array also supported.

  • Patterns compile with `re.I` and match via `re.search` against forward-slash-normalized paths
  • Missing / malformed config → empty list → behavior identical to current default (fully backward-compatible)

Two integration points:

  1. `find_reason_for_path()` short-circuits via `is_excluded_path(n)`
  2. `command_hits()` buckets candidates so the fallback whole-command regex is suppressed when any candidate was excluded. Without this bucket logic, a command like `touch /excluded/CLAUDE.md` would still leak: bare tokens like `touch` stay in `non_excluded_candidates`, the per-candidate loop finds no match, and the fallback regex substring-matches `/CLAUDE.md` against the literal command, re-firing the advisory. The fix gates the fallback on `if not excluded_hits`.

Tests

Adds 9 new cases to `evals/test-integrity-guard.sh` covering:

  • Excluded paths stay silent for Write / Edit / Bash mutating commands
  • Non-excluded paths still trigger advisories (no regression)
  • Second exclusion pattern matches independently
  • Memory paths outside the exclusion list still trigger (boundary)
  • Malformed config silently falls back to default behavior
  • Bare JSON array config format works

Suite goes 19/19 → 28/28 passing. No existing test regression.

```
$ bash evals/test-integrity-guard.sh
...

Passed: 28
Failed: 0
Total: 28

```

Docs

Adds an FAQ section (`docs/FAQ.md`) explaining the config file format, semantics, and typical derived-artifact use case.

Why not just edit `PROTECTED_WRITE_PATTERNS` to be narrower?

The existing patterns are intentionally conservative — they're correct for the typical agent workspace. The drift cases (derived views, build caches) are user-specific and should be opt-in per-environment, not hardcoded. A config-driven allowlist preserves the safe default while giving advanced users an escape hatch.

Co-author note

Drafted with `Claude Opus 4.7 (1M context)` while building a paired skill pack for derived-artifact CLAUDE.md governance (Fearvox/derived-claude-md) — the use case that motivated this PR.

Adds support for a user-supplied allowlist of regex path patterns that
exempt matching paths from ALL persistent-memory / protected-write
advisories in `hooks/integrity-guard.sh`.

## Problem

PUA Integrity Guard fires an advisory on any `Write` / `Edit` / `Bash`
mutating command that touches a `CLAUDE.md`, `memory/` path, or
`.claude/settings.json` — anywhere on the filesystem. This is correct
for canonical project governance files, but is **noisy for derived /
projected directories** (symlink views, build caches, mirrors,
generated docs) where the same-named files are governance pointers to
upstream, not canonical persistent memory themselves.

Concrete reproducer: a user maintains `~/projects/myrepo/views/admin/CLAUDE.md`
that is a guardrail pointer to `~/projects/myrepo/04-memory/`. Every
`touch` / `Write` / `Edit` on the view's `CLAUDE.md` fires advisory,
even though the write is pre-authorized and the file isn't itself
canonical memory.

There is currently no way to suppress this without disabling Integrity
Guard wholesale (`always_on: false`) or editing the cached script
directly (lost on next plugin upgrade).

## Fix

Read `~/.pua/integrity-guard-exclusions.json` (overridable via
`$PUA_INTEGRITY_EXCLUSIONS`). Format:

```json
{"patterns": ["(^|/)derived-view-[^/]+/CLAUDE\\.md$"]}
```

Bare JSON array also supported.

- Patterns compile with `re.I` and match via `re.search` against
  forward-slash-normalized paths
- Missing / malformed config → empty list → behavior identical to
  current default (fully backward-compatible)

Two integration points:
1. `find_reason_for_path()` short-circuits via `is_excluded_path(n)`
2. `command_hits()` buckets candidates so the fallback whole-command
   regex is suppressed when any candidate was excluded (otherwise
   `touch /excluded/CLAUDE.md` falls through and the fallback
   substring-matches `/CLAUDE.md` against the literal command,
   re-firing the advisory)

## Tests

Adds 9 new cases to `evals/test-integrity-guard.sh` covering:
- Excluded paths stay silent for Write / Edit / Bash mutating commands
- Non-excluded paths still trigger advisories (no regression)
- Second exclusion pattern matches independently
- Memory paths outside the exclusion list still trigger (boundary)
- Malformed config silently falls back to default behavior
- Bare JSON array config format works

Suite goes 19/19 → 28/28 passing. No existing test regression.

## Docs

Adds an FAQ section explaining the config file format, semantics, and
the typical derived-artifact use case.
Copilot AI review requested due to automatic review settings May 23, 2026 20:15
Fearvox added a commit to Fearvox/derived-claude-md that referenced this pull request May 23, 2026
…stream PR

Captures the actual deployment of this plugin from local dev → public GitHub →
skills.sh-compatible install. Includes:

  - Install path A: project-scoped npx skills add (from EverOS cwd)
  - Install path B: --global install (from ~), and how it auto-replaced
    dev-era ~/.claude/skills/<name>/ directories with symlinks to the
    central ~/.agents/skills/<name>/ source-of-truth
  - SHA-identical verification: ~/.agents/skills/.../SKILL.md ===
    ~/repos/derived-claude-md/skills/.../SKILL.md
  - Project-scope cleanup with `skills remove --project -y`
  - skills.sh registration via GitHub topics (skills-sh discovery signal)

Also documents the cross-repo work the deployment triggered:

  - tanweai/pua#175: paired upstream PR adding user-configurable path
    exclusion list to PUA Integrity Guard (generalizes the local
    derived-artifact workaround used here)
  - Local PUA patch development trace including the bucketing-fix
    iteration that the upstream PR's test suite caught

Adds a from-scratch deployment reproduction recipe at the bottom.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a user-configurable exclusion allowlist so specific paths can be exempted from Integrity Guard advisories (via env var or a config file), reducing noise for derived/mirrored views.

Changes:

  • Added PUA_INTEGRITY_EXCLUSIONS / ~/.pua/integrity-guard-exclusions.json support to compile exclusion regexes and short-circuit checks.
  • Updated command/path advisory logic to ignore excluded candidates and avoid fallback whole-command matches in some cases.
  • Added eval coverage and FAQ documentation for configuring exclusions.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
hooks/integrity-guard.sh Implements loading/compiling exclusion patterns and applies them to path + mutating-command advisory logic.
evals/test-integrity-guard.sh Adds regression tests for exclusions (file path tools + Bash) plus malformed/array config cases.
docs/FAQ.md Documents how to configure exclusions and what behavior changes to expect.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hooks/integrity-guard.sh
Comment on lines +248 to +263
# Bucket candidates so we can ignore excluded paths at every layer.
excluded_hits = [c for c in candidates if is_excluded_path(c)]
non_excluded_candidates = [c for c in candidates if not is_excluded_path(c)]
for candidate in non_excluded_candidates:
for rx, reason in PROTECTED_WRITE_PATTERNS:
if rx.search(norm_path(candidate)):
return 'advisory', reason, candidate
for rx, reason in PROTECTED_WRITE_PATTERNS:
m = rx.search(normalized)
if m:
return 'advisory', reason, m.group(0)
# Fallback whole-command regex: skip if the command involved any
# excluded path — the regex would just rediscover it via substring
# match (e.g. `touch /repo/excluded-view/CLAUDE.md` re-matching
# `(^|/)CLAUDE\.md$` against the literal command string).
if not excluded_hits:
for rx, reason in PROTECTED_WRITE_PATTERNS:
m = rx.search(normalized)
if m:
return 'advisory', reason, m.group(0)
Comment thread hooks/integrity-guard.sh
Comment on lines +249 to +250
excluded_hits = [c for c in candidates if is_excluded_path(c)]
non_excluded_candidates = [c for c in candidates if not is_excluded_path(c)]

OUT=$(run_guard_with_exclusions Bash '{"command":"touch /repo/CLAUDE.md"}')
assert_advisory "non-excluded path bash mutating command still advisory" "$OUT" "Persistent-memory risk"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants