Skip to content

Add optional filter command for second-stage rule applicability#2

Merged
browep merged 2 commits into
mainfrom
feat/rule-filter-command
Jun 30, 2026
Merged

Add optional filter command for second-stage rule applicability#2
browep merged 2 commits into
mainfrom
feat/rule-filter-command

Conversation

@finneyai-app

@finneyai-app finneyai-app Bot commented Jun 29, 2026

Copy link
Copy Markdown

Summary

Adds an optional filter command to rules — a second-stage applicability check that runs after a rule's globs match. Globs match file paths; a filter lets a rule also depend on file content or relationships between the changed files (e.g. "only apply when a matched file actually contains a SQL string").

The filter runs once per rule with the glob-matched paths appended as argv, and decides applicability via grep-style exit codes:

Exit Meaning Effect
0 pass rule applies
1 clean rejection rule skipped ("<name> (filtered)")
anything else / timeout / missing command error fail-open — rule applies, surfaced in warnings

Fail-open is deliberate: a broken or missing filter must never silently suppress a rule.

---
description: No raw SQL in repositories
globs:
  - 'src/repositories/**/*.ts'
filter: "grep -ilq 'select \\|insert \\|update '"
---
Use the query builder, not raw SQL strings, in repository classes.

Changes

Core

  • types.tsAgentRule.filter, FilterResult/FilterExecutor, RunOptions knobs (runFilters, filterTimeoutMs, filterExecutor, cwd), ReviewResult.warnings
  • rule.ts — parse filter: front-matter (quote-stripped; whitespace-only ⇒ unset)
  • filter-exec.ts (new) — default subprocess executor: tokenizes the command, appends matched paths, stdin closed, AGENT_RULES_SUBPROCESS=1 recursion marker; maps exit codes to FilterResult
  • runner.ts — concurrent filter stage in discoverApplicableRules (only spawns for rules that passed the glob stage); threads options + warnings through runReview
  • cli.ts--no-filters and --filter-timeout; warnings to stderr / JSON. --list honors filters
  • index.ts — new public exports

Trust model: filter executes arbitrary commands with the user's privileges — the same trust level the rules directory already carries. For reviewing untrusted diffs (e.g. fork PRs that could edit a filter), --no-filters disables all filter execution.

Tests (14 added, 50 total)

  • parsing, discovery (pass/reject/error/throw/runFilters:false, executor called only for filtered rules with correct paths), real subprocess exit-code mapping, smoke (--no-filters)

Docker

  • image now copies test/ and the entrypoint gains test (vitest) and check (unit tests + smoke) commands; docker/README.md updated. Previously the image could only run smoke.

Docs: README.md, docs/agent-rules-requirements.md (R32–R38), CHANGELOG.md, plus examples/rules/no-raw-sql.md and examples/filters/touches-public-api.sh.

Test plan

All hermetic, verified locally and in Docker:

docker build -t agent-rules .
docker run --rm agent-rules check   # 50 unit tests + 9 smoke checks ✅

Also green: yarn typecheck, yarn lint, yarn format:check.

🤖 Generated with Claude Code

browep added 2 commits June 29, 2026 15:27
Globs decide applicability by file path; the new `filter` front-matter field
adds a content/relationship-aware check that runs after a glob match. It runs
once per rule with the matched paths appended as argv and decides via grep-style
exit codes: 0 applies, 1 skips, anything else / timeout / missing command fails
open and applies (errors must never silently suppress a rule).

- types: AgentRule.filter, FilterResult/FilterExecutor, RunOptions knobs
  (runFilters, filterTimeoutMs, filterExecutor, cwd), ReviewResult.warnings
- rule.ts: parse `filter:` (quote-stripped; whitespace-only => unset)
- filter-exec.ts: default subprocess executor (stdin closed, recursion marker)
- runner.ts: concurrent filter stage in discoverApplicableRules; warnings
- cli.ts: --no-filters and --filter-timeout; warnings to stderr/JSON
- tests: parsing, discovery (pass/reject/error/throw/disabled), exec mapping
- docs/examples/smoke updated

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The image built and ran only the smoke script; the unit tests could not run
because `test/` was never copied in and the entrypoint had no test command.

- Dockerfile: COPY test ./test (vitest is already an installed devDependency)
- entrypoint.sh: `test` (vitest) and `check` (unit tests + smoke) commands
- docker/README.md: document the hermetic test commands

Verified: `docker run --rm agent-rules check` => 50 tests + 9 smoke checks pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@browep browep force-pushed the feat/rule-filter-command branch from e96bdb2 to 277f7bc Compare June 29, 2026 21:27
@browep browep merged commit 4800661 into main Jun 30, 2026
3 checks passed
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