Skip to content

Commit 21ee839

Browse files
release: 2.2.0 (#24)
* fix(permissions): rank a wildcard by what was measured to ride in it crowd.dev's settings produced 22 identical word-boundary warnings, and exactly one of the 22 rules grants remote writes. The one that deletes a branch read no louder than git logfoo. Three measured mechanics now set the volume. A wildcarded gh api allow rule is a write grant: flags ride any star, and gh switches GET to POST the moment a parameter is added — Bash(gh api repos*) auto-approved -X DELETE, -f description=x, and a git/refs branch deletion. A mid-rule star spans words, so one standing before the runner's subcommand admits an exec form: pnpm --filter web exec rm -rf ./x build was auto-approved by Bash(pnpm --filter * build). And fusion is ranked by reach: a one-word prefix swaps the program itself (python* covers python3), anything longer must share the prefix and drops to info. One fact per rule: the sharpest statement about a wildcard suppresses the word-boundary nag on the same rule. Deliberately not curl: the same flags ride its wildcards, but unlock writes nothing in the command is authorised to make. * release: 2.2.0 A minor by the project's own precedent: a repository that reported nothing can now report a wildcarded gh api rule as a write grant, and two thirds of the corpus's word-boundary warnings drop to info. Every new sentence carries the measurement that grounds it. Internal ranges move with the version, as they did the last two times. --------- Co-authored-by: Dennis Havermans <dennis.havermans@ae.be>
1 parent 2f14fc9 commit 21ee839

8 files changed

Lines changed: 321 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,51 @@ Versioning follows [Semantic Versioning](https://semver.org/).
99

1010
## [Unreleased]
1111

12+
## [2.2.0] — 2026-09-03
13+
14+
A ranking release. 2.1.1 made every finding true; this one makes the volume
15+
track the consequence. Running the permission audit over the Linux Foundation's
16+
crowd.dev produced 22 identical word-boundary warnings, and exactly one of the
17+
22 rules — `Bash(gh api repos*)` — grants remote writes. The one that can
18+
delete a branch read no louder than `git logfoo`. That is the wall of noise the
19+
pattern set's own comment warns about, so this release ranks the wildcard
20+
findings by what was measured to ride in the wildcard.
21+
22+
### Added
23+
24+
- **`AGF506`: a wildcarded `gh api` allow rule is reported as a write grant.**
25+
A `*` matches any characters including spaces, so method and parameter flags
26+
ride wherever it stands, and gh's own help documents that adding a parameter
27+
switches the request from GET to POST. Measured on Claude Code 2.1.238:
28+
`Bash(gh api repos*)` auto-approved `-X DELETE`, `-f description=x`, and a
29+
branch deletion through `git/refs`. Deliberately scoped to `gh api`, which
30+
sends the user's token on every call — the same flags ride a wildcarded curl
31+
rule, but unlock writes nothing in the command is authorised to make.
32+
- **`AGF506`: a mid-rule wildcard that stands where the runner's subcommand
33+
goes.** Measured: `pnpm --filter web exec rm -rf ./x build` is auto-approved
34+
by `Bash(pnpm --filter * build)`, because a mid-rule `*` spans multiple
35+
space-separated words. The check stays quiet once the subcommand is pinned
36+
before the first star, so `Bash(npx rhachet run --skill x --glob '*.ts')`
37+
reports nothing.
38+
39+
### Changed
40+
41+
- **Word-boundary findings are ranked by what the fusion can reach.** A
42+
one-word prefix fuses the program name itself — `Bash(python*)` silently
43+
covers `python3 -c` with anything after it — and stays a warning. After the
44+
first word the fused text must still share the prefix, so `Bash(git log*)`
45+
drops to info: `git logfoo` is nobody's command. Across the 344-file
46+
permission corpus this moves roughly two thirds of all permission findings
47+
from warning to info while 34 wildcarded `gh api` rules surface as the write
48+
grants they are.
49+
- **One fact per rule.** A rule whose wildcard admits writes or chooses the
50+
subcommand is no longer additionally nagged about its word boundary; the
51+
wildcard-before-subcommand finding now carries its measured consequence
52+
(`Bash(git * main)` auto-approves `git push --force origin main`).
53+
Severity defaults remain documented as not stable in
54+
[docs/stability.md](docs/stability.md); pin them in `agentfile.yaml` if CI
55+
depends on them.
56+
1257
## [2.1.1] — 2026-09-01
1358

1459
2.1.0 shipped with a claim it had only partly earned. The lesson of that

docs/diagnostics.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,24 @@ not:
471471

472472
* **`Bash(ls*)` also matches `lsof`.** A `*` with no space before it enforces no
473473
word boundary. `Bash(ls *)` — one space different — matches only `ls` with
474-
arguments. As an allow rule this grants more than it appears to.
474+
arguments. Ranked by what the fusion can reach: a one-word prefix fuses the
475+
program name itself (`Bash(python*)` covers `python3` with any arguments) and
476+
is a warning; after the first word the fused text must still share the prefix,
477+
so `Bash(git log*)` is informational.
478+
* **A wildcarded `gh api` allow rule grants writes, not just reads.** A `*`
479+
matches any characters including spaces, so method and parameter flags ride
480+
wherever it stands — and gh switches GET to POST the moment a parameter is
481+
added. Measured: `Bash(gh api repos*)` auto-approves `-X DELETE`,
482+
`-f description=x`, and deleting a branch through `git/refs`.
483+
* **A mid-rule `*` can choose what a runner executes.** It spans multiple
484+
space-separated words, so a wildcard standing before the subcommand is pinned
485+
admits an exec form and everything after it. Measured:
486+
`pnpm --filter web exec rm -rf ./x build` was auto-approved by
487+
`Bash(pnpm --filter * build)`. Quiet once the subcommand is pinned before the
488+
first star.
489+
490+
When one rule triggers several of these, only the sharpest is reported — a
491+
rule whose wildcard admits writes is not also nagged about word boundaries.
475492
* **`:*` is recognised only at the end of a pattern.** In `Bash(git:* push)` the
476493
colon is literal and the rule matches nothing. Reported as an error: the rule
477494
has no effect and nothing says so at load time.

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/agentfile/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agentfile/agentfile",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"description": "Find what is wrong with the AI agent configuration your repository already has",
55
"type": "module",
66
"bin": {
@@ -42,7 +42,7 @@
4242
"access": "public"
4343
},
4444
"dependencies": {
45-
"@agentfile/cli": "^2.1.1"
45+
"@agentfile/cli": "^2.2.0"
4646
},
4747
"devDependencies": {
4848
"@types/node": "^22.19.0",

packages/cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agentfile/cli",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"description": "Find what is wrong with the AI agent configuration your repository already has — doctor, check, lint, audit, adopt, compile, eval",
55
"type": "module",
66
"main": "./dist/bin.js",
@@ -45,7 +45,7 @@
4545
"access": "public"
4646
},
4747
"dependencies": {
48-
"@agentfile/core": "^2.1.1",
48+
"@agentfile/core": "^2.2.0",
4949
"chalk": "^6.0.0",
5050
"commander": "^14.0.3",
5151
"enquirer": "^2.4.1"

packages/core/__tests__/security.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1075,6 +1075,74 @@ describe("auditPermissions", () => {
10751075
});
10761076
});
10771077

1078+
// ─── ranking: the sharpest statement about a wildcard wins ─────────────
1079+
1080+
describe("wildcard consequence ranking", () => {
1081+
// crowd.dev's settings: 22 rules with trailing stars, of which exactly one
1082+
// grants remote writes. Measured on Claude Code 2.1.238: under
1083+
// Bash(gh api repos*), `-X DELETE`, `-f description=x` (gh switches GET to
1084+
// POST when parameters are added), and a git/refs branch deletion were all
1085+
// auto-approved.
1086+
it("raises gh api ride-along above the word-boundary noise", () => {
1087+
const findings = audit([rule("allow", "Bash(gh api repos*)")]);
1088+
expect(findings).toHaveLength(1);
1089+
expect(findings[0].severity).toBe("warning");
1090+
expect(findings[0].data?.problem).toBe("api-method-ride-along");
1091+
expect(findings[0].message).toContain("writes, not just reads");
1092+
});
1093+
1094+
it("stays quiet on an exact gh api endpoint, which no flag can ride", () => {
1095+
expect(audit([rule("allow", "Bash(gh api repos/o/r/pulls/42/comments --paginate)")])).toHaveLength(0);
1096+
});
1097+
1098+
// prisma's shape. Measured: `pnpm --filter web exec rm -rf ./x build` was
1099+
// auto-approved by Bash(pnpm --filter * build) and the trailing-star form.
1100+
it("reports a wildcard standing where the runner's subcommand goes", () => {
1101+
const findings = audit([rule("allow", "Bash(pnpm --filter * test*)")]);
1102+
expect(findings).toHaveLength(1);
1103+
expect(findings[0].severity).toBe("warning");
1104+
expect(findings[0].data?.problem).toBe("exec-admitting-wildcard");
1105+
});
1106+
1107+
it("stays quiet once the subcommand is pinned before the star", () => {
1108+
expect(audit([rule("allow", "Bash(npx rhachet run --skill sedreplace --glob src/*.ts)")])).toHaveLength(0);
1109+
});
1110+
1111+
it("does not double-report fusion on a rule already carrying a sharper fact", () => {
1112+
const problems = audit([rule("allow", "Bash(gh api repos*)"), rule("allow", "Bash(pnpm --filter * build*)")]).map(
1113+
(item) => item.data?.problem,
1114+
);
1115+
expect(problems).toEqual(["api-method-ride-along", "exec-admitting-wildcard"]);
1116+
});
1117+
1118+
// rodekruis's rule. Measured: git push --force origin main and
1119+
// git push --delete origin main were both auto-approved under it.
1120+
it("names the measured consequence when the wildcard is the verb", () => {
1121+
const [found] = audit([rule("allow", "Bash(git * main)")]);
1122+
expect(found.data?.problem).toBe("wildcard-before-subcommand");
1123+
expect(found.explanation).toContain("git push --force origin main");
1124+
});
1125+
1126+
// The fused text must share the prefix, so `git logfoo` is nobody's
1127+
// command — but a one-word prefix fuses the program name itself, and
1128+
// `python*` covers `python3 -c` with anything after it.
1129+
it("ranks fusion by what it can reach", () => {
1130+
const single = audit([rule("allow", "Bash(python*)")]);
1131+
expect(single[0].severity).toBe("warning");
1132+
expect(single[0].data?.consequence).toBe("program-substitution");
1133+
1134+
const multi = audit([rule("allow", "Bash(git log*)")]);
1135+
expect(multi[0].severity).toBe("info");
1136+
expect(multi[0].data?.consequence).toBe("prefix-fusion");
1137+
});
1138+
1139+
it("leaves a trailing star on a runner to the fusion check, where it belongs", () => {
1140+
const [found] = audit([rule("allow", "Bash(pnpm test*)")]);
1141+
expect(found.data?.problem).toBe("missing-word-boundary");
1142+
expect(found.severity).toBe("info");
1143+
});
1144+
});
1145+
10781146
// ─── fragile argument patterns ─────────────────────────────────────────
10791147

10801148
describe("fragile argument patterns", () => {

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agentfile/core",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"description": "Read, analyse and compile AI agent configuration — discovery, a normalized IR, a resolver, diagnostics, and static security analysis",
55
"type": "module",
66
"main": "./dist/index.js",

0 commit comments

Comments
 (0)