@agenttrail/guardrails 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -205,7 +205,7 @@ var acInstructionFileEdit = {
205
205
  severity: "high",
206
206
  defaultAction: "require_approval",
207
207
  title: "The agent editing its standing instructions",
208
- description: "Holds a file tool opening the instructions a coding agent loads at the start of every session: `CLAUDE.md`, `CLAUDE.local.md` and `.claude/rules/`; `AGENTS.md` and `AGENTS.override.md`, which Codex, Cursor, Windsurf, Copilot and Cline all read; `GEMINI.md`; `.cursorrules`; Windsurf's `.windsurfrules`, `.windsurf/rules/` and `.devin/rules/`; Cline's `.clinerules` file or directory and its global `Cline/Rules/` folder; Copilot's `.github/copilot-instructions.md` and `.github/instructions/**/*.instructions.md`; and `.aider.conf.yml`, which sets the files Aider reads on every launch. A line written into one of these is followed in every later session. Matched in any directory and in any letter case. File tools carry a path and no content, so it cannot see what was written, and it does not tell reading apart from editing. Does not cover `.cursor/rules/`, which `fs.agent-self-config` holds. Misses a context file renamed through Gemini's `context.fileName` or Codex's `project_doc_fallback_filenames`, Aider's `CONVENTIONS.md`, which Aider loads only when asked and which is too common a name to match, and any of these files written by a shell command instead of a file tool.",
208
+ description: "Holds a file tool opening the instructions a coding agent loads at the start of every session: `CLAUDE.md`, `CLAUDE.local.md` and `.claude/rules/`; `AGENTS.md` and `AGENTS.override.md`, which Codex, Cursor, Windsurf, Copilot and Cline all read; `GEMINI.md`; `.cursorrules`; Windsurf's `.windsurfrules`, `.windsurf/rules/` and `.devin/rules/`; Cline's `.clinerules` file or directory and its global `Cline/Rules/` folder; Copilot's `.github/copilot-instructions.md` and `.github/instructions/**/*.instructions.md`; and `.aider.conf.yml`, which sets the files Aider reads on every launch. A line written into one of these is followed in every later session. Matched in any directory and in any letter case. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so an agent opening its instructions to read them is not held; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot see what was written. Does not cover `.cursor/rules/`, which `fs.agent-self-config` holds. Misses a context file renamed through Gemini's `context.fileName` or Codex's `project_doc_fallback_filenames`, Aider's `CONVENTIONS.md`, which Aider loads only when asked and which is too common a name to match, and any of these files written by a shell command instead of a file tool.",
209
209
  match: {
210
210
  any_of: [
211
211
  {
@@ -221,6 +221,11 @@ var acInstructionFileEdit = {
221
221
  { kind: "execute_tool", file_glob: "**/.github/copilot-instructions.md" },
222
222
  { kind: "execute_tool", file_glob: "**/.github/instructions/**/*.instructions.md" },
223
223
  { kind: "execute_tool", file_glob: "**/.aider.conf.yml" }
224
+ ],
225
+ none_of: [
226
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
227
+ // the write tools, so an unknown file tool on a matched path still holds.
228
+ { kind: "execute_tool", label: "{Read,Grep}" }
224
229
  ]
225
230
  },
226
231
  fixtures: {
@@ -255,7 +260,9 @@ var acInstructionFileEdit = {
255
260
  file(".github/workflows/ci.yml"),
256
261
  file(".cursor/rules/style.mdc"),
257
262
  file(".claude/settings.json"),
258
- file(".aider.chat.history.md")
263
+ file(".aider.chat.history.md"),
264
+ file("CLAUDE.md", "Read"),
265
+ file(".github/copilot-instructions.md", "Grep")
259
266
  ]
260
267
  }
261
268
  };
@@ -269,7 +276,7 @@ var acMcpServerAdd = {
269
276
  severity: "high",
270
277
  defaultAction: "require_approval",
271
278
  title: "Adding an MCP server to an agent",
272
- description: "Holds the commands that register an MCP server with a coding agent \u2014 `claude mcp add`, `claude mcp add-json`, `claude mcp add-from-claude-desktop`, `codex mcp add` and `gemini mcp add` \u2014 and `claude --mcp-config`, which attaches servers to a single session. Each gives an agent a new set of tools, often a program fetched and started on the spot, with nobody reviewing the change. Deliberately NOT matched: listing or removing servers (`claude mcp list`, `claude mcp remove`), and the MCP Inspector (`npx @modelcontextprotocol/inspector`), which is a debugging tool rather than a registration. Cursor has no command for this, so its `.cursor/mcp.json`, in a project or the home directory, is matched as a file instead; a project's `.mcp.json` is held by `fs.agent-self-config`. Misses servers written into `~/.claude.json` or Gemini's `settings.json` with a file tool, and a global flag whose value is quoted when it sits before `mcp`. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone, as long as every shell metacharacter stays inside the quotes.",
279
+ description: "Holds the commands that register an MCP server with a coding agent \u2014 `claude mcp add`, `claude mcp add-json`, `claude mcp add-from-claude-desktop`, `codex mcp add` and `gemini mcp add` \u2014 and `claude --mcp-config`, which attaches servers to a single session. Each gives an agent a new set of tools, often a program fetched and started on the spot, with nobody reviewing the change. Deliberately NOT matched: listing or removing servers (`claude mcp list`, `claude mcp remove`), and the MCP Inspector (`npx @modelcontextprotocol/inspector`), which is a debugging tool rather than a registration. Cursor has no command for this, so its `.cursor/mcp.json`, in a project or the home directory, is matched as a file instead \u2014 written, not read: the `Read` and `Grep` tools never match it, and every other file tool does, including one this corpus does not know; a project's `.mcp.json` is held by `fs.agent-self-config`. Misses servers written into `~/.claude.json` or Gemini's `settings.json` with a file tool, and a global flag whose value is quoted when it sits before `mcp`. A quoted MENTION is not a use: a search, a `git commit -m` message, an `echo` or a `curl --data` body that only names this command is left alone, as long as every shell metacharacter stays inside the quotes.",
273
280
  match: {
274
281
  any_of: [
275
282
  {
@@ -284,7 +291,14 @@ var acMcpServerAdd = {
284
291
  // Cursor has no `mcp add` command; its servers are added by editing this file.
285
292
  { kind: "execute_tool", file_glob: "**/.cursor/mcp.json" }
286
293
  ],
287
- none_of: [...QUOTED_MENTION]
294
+ none_of: [
295
+ // A read is not an addition. This covers the `.cursor/mcp.json` arm — the
296
+ // shell arm already pins its own tool family. Named as a deny-list rather
297
+ // than an `any_of` over the write tools, so an unknown file tool on that
298
+ // path still holds.
299
+ { kind: "execute_tool", label: "{Read,Grep}" },
300
+ ...QUOTED_MENTION
301
+ ]
288
302
  },
289
303
  fixtures: {
290
304
  block: [
@@ -309,7 +323,9 @@ var acMcpServerAdd = {
309
323
  bash("npx @modelcontextprotocol/inspector"),
310
324
  file("config/mcp.json"),
311
325
  file(".cursor/rules/style.mdc"),
312
- bash("pnpm run test")
326
+ bash("pnpm run test"),
327
+ file(".cursor/mcp.json", "Read"),
328
+ file("/Users/dev/.cursor/mcp.json", "Grep")
313
329
  ]
314
330
  }
315
331
  };
@@ -321,7 +337,7 @@ var acMemoryStoreEdit = {
321
337
  severity: "high",
322
338
  defaultAction: "require_approval",
323
339
  title: "The agent editing its own memory",
324
- description: "Holds a file tool opening the memory a coding agent carries between sessions: Claude Code's auto memory under `.claude/projects/<project>/memory/`, and its sub-agent memory in `.claude/agent-memory/` and `.claude/agent-memory-local/`; Codex's `.codex/memories/`; Gemini's private `.gemini/tmp/<project>/memory/`; Windsurf's `.codeium/windsurf/memories/`, including `global_rules.md`; and `.cursor/memory/`. A false fact saved here is recalled as true in every later session. A `MEMORY.md` outside those directories deliberately does NOT match, and neither does a project's own `docs/memory/` folder: the name alone is not an agent's memory. File tools carry a path and no content, so it cannot see what was written, and it does not tell reading apart from editing, so an agent recalling a memory by reading its file is asked too. Misses a memory directory moved with Claude Code's `autoMemoryDirectory` setting, and Cursor memories kept anywhere other than `.cursor/memory/`, since Cursor does not document where it stores them. Gemini memories saved into `GEMINI.md` are held by `ac.instruction-file-edit` instead.",
340
+ description: "Holds a file tool opening the memory a coding agent carries between sessions: Claude Code's auto memory under `.claude/projects/<project>/memory/`, and its sub-agent memory in `.claude/agent-memory/` and `.claude/agent-memory-local/`; Codex's `.codex/memories/`; Gemini's private `.gemini/tmp/<project>/memory/`; Windsurf's `.codeium/windsurf/memories/`, including `global_rules.md`; and `.cursor/memory/`. A false fact saved here is recalled as true in every later session. A `MEMORY.md` outside those directories deliberately does NOT match, and neither does a project's own `docs/memory/` folder: the name alone is not an agent's memory. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so an agent recalling a memory by reading its file is not asked; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot see what was written. Misses a memory directory moved with Claude Code's `autoMemoryDirectory` setting, and Cursor memories kept anywhere other than `.cursor/memory/`, since Cursor does not document where it stores them. Gemini memories saved into `GEMINI.md` are held by `ac.instruction-file-edit` instead.",
325
341
  match: {
326
342
  any_of: [
327
343
  { kind: "execute_tool", file_glob: "**/.claude/projects/*/memory/**" },
@@ -330,6 +346,11 @@ var acMemoryStoreEdit = {
330
346
  { kind: "execute_tool", file_glob: "**/.gemini/tmp/*/memory/**" },
331
347
  { kind: "execute_tool", file_glob: "**/.codeium/windsurf/memories/**" },
332
348
  { kind: "execute_tool", file_glob: "**/.cursor/memory/**" }
349
+ ],
350
+ none_of: [
351
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
352
+ // the write tools, so an unknown file tool on a matched path still holds.
353
+ { kind: "execute_tool", label: "{Read,Grep}" }
333
354
  ]
334
355
  },
335
356
  fixtures: {
@@ -353,7 +374,9 @@ var acMemoryStoreEdit = {
353
374
  file("packages/memory-store/src/index.ts"),
354
375
  file("/Users/dev/.claude/projects/-Users-dev-shop/transcript.jsonl"),
355
376
  file("/home/dev/.gemini/settings.json"),
356
- file(".claude/settings.json")
377
+ file(".claude/settings.json"),
378
+ file(".claude/agent-memory/reviewer/MEMORY.md", "Read"),
379
+ file("/home/dev/.codex/memories/project.md", "Grep")
357
380
  ]
358
381
  }
359
382
  };
@@ -423,7 +446,7 @@ var acSkillInstall = {
423
446
  severity: "high",
424
447
  defaultAction: "require_approval",
425
448
  title: "The agent installing a skill, command or sub-agent",
426
- description: "Holds a file tool opening a skill, slash command, sub-agent or output style that a coding agent loads by name: Claude Code's `.claude/skills/`, `.claude/commands/`, `.claude/agents/` and `.claude/output-styles/`; the shared `.agents/skills/`; Codex's `.codex/skills/`, `.codex/prompts/` and `.codex/agents/`; Gemini's `.gemini/commands/`, `.gemini/skills/` and `.gemini/agents/`; Cursor's `.cursor/skills/`, `.cursor/agents/` and `.cursor/commands/`; Windsurf's `.windsurf/workflows/` and `.windsurf/skills/` and their global copies under `.codeium/windsurf/`; Cline's `.cline/skills/`; and a `SKILL.md` anywhere, which is how a plugin ships a skill. Each becomes a reusable instruction the agent may follow later, often with a script beside it that the agent runs. Matched at project or home level and in any letter case. File tools carry a path and no content, so it cannot see what was written, and it does not tell reading apart from editing. Deliberately NOT matched: an ordinary `skills/`, `commands/` or `agents/` folder in a project's source, and `.clinerules/skills/`, which `ac.instruction-file-edit` holds. Misses a skill copied into one of these folders by a shell command such as `cp` or `git clone`, and the managed system-wide workflow folders.",
449
+ description: "Holds a file tool opening a skill, slash command, sub-agent or output style that a coding agent loads by name: Claude Code's `.claude/skills/`, `.claude/commands/`, `.claude/agents/` and `.claude/output-styles/`; the shared `.agents/skills/`; Codex's `.codex/skills/`, `.codex/prompts/` and `.codex/agents/`; Gemini's `.gemini/commands/`, `.gemini/skills/` and `.gemini/agents/`; Cursor's `.cursor/skills/`, `.cursor/agents/` and `.cursor/commands/`; Windsurf's `.windsurf/workflows/` and `.windsurf/skills/` and their global copies under `.codeium/windsurf/`; Cline's `.cline/skills/`; and a `SKILL.md` anywhere, which is how a plugin ships a skill. Each becomes a reusable instruction the agent may follow later, often with a script beside it that the agent runs. Matched at project or home level and in any letter case. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so an agent opening an installed skill to read it is not held; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot see what was written. Deliberately NOT matched: an ordinary `skills/`, `commands/` or `agents/` folder in a project's source, and `.clinerules/skills/`, which `ac.instruction-file-edit` holds. Misses a skill copied into one of these folders by a shell command such as `cp` or `git clone`, and the managed system-wide workflow folders.",
427
450
  match: {
428
451
  any_of: [
429
452
  { kind: "execute_tool", file_glob: "**/.claude/{skills,commands,agents,output-styles}/**" },
@@ -435,6 +458,11 @@ var acSkillInstall = {
435
458
  { kind: "execute_tool", file_glob: "**/.codeium/windsurf/{skills,global_workflows}/**" },
436
459
  { kind: "execute_tool", file_glob: "**/.cline/skills/**" },
437
460
  { kind: "execute_tool", file_glob: "**/SKILL.md" }
461
+ ],
462
+ none_of: [
463
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
464
+ // the write tools, so an unknown file tool on a matched path still holds.
465
+ { kind: "execute_tool", label: "{Read,Grep}" }
438
466
  ]
439
467
  },
440
468
  fixtures: {
@@ -464,7 +492,9 @@ var acSkillInstall = {
464
492
  file(".claude/settings.json"),
465
493
  file(".github/workflows/release.yml"),
466
494
  file("scripts/release.sh"),
467
- file(".gemini/settings.json")
495
+ file(".gemini/settings.json"),
496
+ file(".claude/skills/deploy/SKILL.md", "Read"),
497
+ file(".claude/commands/deploy.md", "Grep")
468
498
  ]
469
499
  }
470
500
  };
@@ -1056,7 +1086,7 @@ var fsAgentSelfConfig = {
1056
1086
  severity: "high",
1057
1087
  defaultAction: "require_approval",
1058
1088
  title: "The agent editing its own configuration",
1059
- description: "Holds a file tool opening the files that define what the agent itself is allowed to do \u2014 Claude Code settings and hooks, an MCP server list, a plugin manifest, Cursor rules, a Codex config, and the guard's own config.json and guardrails.json. An agent that can edit these can widen its own reach with nobody reviewing the change. Does NOT match ordinary project files, or `.claude/commands/*.md`, which are prompts rather than permissions. Bounded to WELL-KNOWN paths: no working directory or project root reaches the guard, so it can only match names it already knows.",
1089
+ description: "Holds a file tool opening the files that define what the agent itself is allowed to do \u2014 Claude Code settings and hooks, an MCP server list, a plugin manifest, Cursor rules, a Codex config, and the guard's own config.json and guardrails.json. An agent that can edit these can widen its own reach with nobody reviewing the change. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so opening one of these files to read it is not held; every other file tool is, including one this corpus does not know. Does NOT match ordinary project files, or `.claude/commands/*.md`, which are prompts rather than permissions. Bounded to WELL-KNOWN paths: no working directory or project root reaches the guard, so it can only match names it already knows.",
1060
1090
  match: {
1061
1091
  any_of: [
1062
1092
  { kind: "execute_tool", file_glob: "**/.claude/settings*.json" },
@@ -1067,6 +1097,11 @@ var fsAgentSelfConfig = {
1067
1097
  { kind: "execute_tool", file_glob: "**/.codex/config.toml" },
1068
1098
  { kind: "execute_tool", file_glob: "**/.agenttrail/guard/config.json" },
1069
1099
  { kind: "execute_tool", file_glob: "**/.agenttrail/guard/guardrails.json" }
1100
+ ],
1101
+ none_of: [
1102
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1103
+ // the write tools, so an unknown file tool on a matched path still holds.
1104
+ { kind: "execute_tool", label: "{Read,Grep}" }
1070
1105
  ]
1071
1106
  },
1072
1107
  fixtures: {
@@ -1080,7 +1115,9 @@ var fsAgentSelfConfig = {
1080
1115
  file(".claude/commands/deploy.md"),
1081
1116
  file("src/index.ts"),
1082
1117
  file("package.json"),
1083
- file("README.md")
1118
+ file("README.md"),
1119
+ file(".claude/settings.json", "Read"),
1120
+ file(".mcp.json", "Grep")
1084
1121
  ]
1085
1122
  }
1086
1123
  };
@@ -1092,7 +1129,7 @@ var fsCiDefinition = {
1092
1129
  severity: "medium",
1093
1130
  defaultAction: "require_approval",
1094
1131
  title: "Editing the CI pipeline definition",
1095
- description: "Holds a file tool opening a CI definition \u2014 GitHub Actions workflows and composite actions, .gitlab-ci.yml, a Jenkinsfile, CircleCI, Azure Pipelines, Buildkite or Bitbucket pipelines. This is where the checks that gate every merge are written down, and where a new step would run with the repository's secrets; both edits look like an ordinary diff. Does NOT match other files under .github/ (CODEOWNERS, issue templates), which gate nothing. It MISSES a CI system whose definition lives outside the repository.",
1132
+ description: "Holds a file tool opening a CI definition \u2014 GitHub Actions workflows and composite actions, .gitlab-ci.yml, a Jenkinsfile, CircleCI, Azure Pipelines, Buildkite or Bitbucket pipelines. This is where the checks that gate every merge are written down, and where a new step would run with the repository's secrets; both edits look like an ordinary diff. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so opening a CI definition to read it is not held; every other file tool is, including one this corpus does not know. Does NOT match other files under .github/ (CODEOWNERS, issue templates), which gate nothing. It MISSES a CI system whose definition lives outside the repository.",
1096
1133
  match: {
1097
1134
  any_of: [
1098
1135
  { kind: "execute_tool", file_glob: "**/.github/workflows/**" },
@@ -1103,6 +1140,11 @@ var fsCiDefinition = {
1103
1140
  { kind: "execute_tool", file_glob: "**/azure-pipelines.yml" },
1104
1141
  { kind: "execute_tool", file_glob: "**/.buildkite/**" },
1105
1142
  { kind: "execute_tool", file_glob: "**/bitbucket-pipelines.yml" }
1143
+ ],
1144
+ none_of: [
1145
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1146
+ // the write tools, so an unknown file tool on a matched path still holds.
1147
+ { kind: "execute_tool", label: "{Read,Grep}" }
1106
1148
  ]
1107
1149
  },
1108
1150
  fixtures: {
@@ -1116,7 +1158,9 @@ var fsCiDefinition = {
1116
1158
  file(".github/CODEOWNERS"),
1117
1159
  file(".github/PULL_REQUEST_TEMPLATE.md"),
1118
1160
  file("package.json"),
1119
- file("README.md")
1161
+ file("README.md"),
1162
+ file(".github/workflows/ci.yml", "Read"),
1163
+ file(".gitlab-ci.yml", "Grep")
1120
1164
  ]
1121
1165
  }
1122
1166
  };
@@ -1128,7 +1172,7 @@ var fsSystemPaths = {
1128
1172
  severity: "high",
1129
1173
  defaultAction: "require_approval",
1130
1174
  title: "Writing to a system directory",
1131
- description: "Holds a file tool opening a path under a system directory \u2014 /etc, /bin, /sbin, /usr/bin, /usr/local/bin, /boot, /System, /Library/LaunchDaemons, or Windows/System32. HONEST CEILING: this is a list of well-known ABSOLUTE paths and it cannot be anything else. No working directory and no project root reaches the guard, so the rule you would actually want \u2014 'the agent wrote outside the project' \u2014 is inexpressible, and would match everything or nothing. It therefore MISSES a write anywhere else outside your repository, including another project on the same machine.",
1175
+ description: "Holds a file tool writing to a path under a system directory \u2014 /etc, /bin, /sbin, /usr/bin, /usr/local/bin, /boot, /System, /Library/LaunchDaemons, or Windows/System32. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so reading /etc/hosts is not held; every other file tool is, including one this corpus does not know. HONEST CEILING: this is a list of well-known ABSOLUTE paths and it cannot be anything else. No working directory and no project root reaches the guard, so the rule you would actually want \u2014 'the agent wrote outside the project' \u2014 is inexpressible, and would match everything or nothing. It therefore MISSES a write anywhere else outside your repository, including another project on the same machine.",
1132
1176
  match: {
1133
1177
  any_of: [
1134
1178
  { kind: "execute_tool", file_glob: "/etc/**" },
@@ -1140,6 +1184,11 @@ var fsSystemPaths = {
1140
1184
  { kind: "execute_tool", file_glob: "/System/**" },
1141
1185
  { kind: "execute_tool", file_glob: "/Library/LaunchDaemons/**" },
1142
1186
  { kind: "execute_tool", file_glob: "**/Windows/System32/**" }
1187
+ ],
1188
+ none_of: [
1189
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1190
+ // the write tools, so an unknown file tool on a matched path still holds.
1191
+ { kind: "execute_tool", label: "{Read,Grep}" }
1143
1192
  ]
1144
1193
  },
1145
1194
  fixtures: {
@@ -1153,7 +1202,9 @@ var fsSystemPaths = {
1153
1202
  file("src/index.ts"),
1154
1203
  file("/home/dev/project/src/main.rs"),
1155
1204
  file("/tmp/scratch.txt", "Write"),
1156
- file("docs/etc-notes.md")
1205
+ file("docs/etc-notes.md"),
1206
+ file("/etc/hosts", "Read"),
1207
+ file("/usr/local/bin/app", "Grep")
1157
1208
  ]
1158
1209
  }
1159
1210
  };
@@ -1165,7 +1216,7 @@ var fsVcsInternals = {
1165
1216
  severity: "high",
1166
1217
  defaultAction: "require_approval",
1167
1218
  title: "Editing git's internals directly",
1168
- description: "Holds a file tool opening git's own bookkeeping \u2014 .git/config, .git/hooks/, .git/refs/, .git/HEAD, .git/info/exclude \u2014 where a change alters what future git commands do rather than what the repository contains. Deliberately NARROW: all of .git/** would include COMMIT_EDITMSG and the index, which change during every ordinary commit, so the rule would fire constantly and be switched off. It does NOT match .gitignore, .gitattributes or anything under .github/, which are tracked project files.",
1219
+ description: "Holds a file tool opening git's own bookkeeping \u2014 .git/config, .git/hooks/, .git/refs/, .git/HEAD, .git/info/exclude \u2014 where a change alters what future git commands do rather than what the repository contains. Deliberately NARROW: all of .git/** would include COMMIT_EDITMSG and the index, which change during every ordinary commit, so the rule would fire constantly and be switched off. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so opening .git/config to read it is not held; every other file tool is, including one this corpus does not know. It does NOT match .gitignore, .gitattributes or anything under .github/, which are tracked project files.",
1169
1220
  match: {
1170
1221
  any_of: [
1171
1222
  { kind: "execute_tool", file_glob: "**/.git/config" },
@@ -1173,6 +1224,11 @@ var fsVcsInternals = {
1173
1224
  { kind: "execute_tool", file_glob: "**/.git/refs/**" },
1174
1225
  { kind: "execute_tool", file_glob: "**/.git/HEAD" },
1175
1226
  { kind: "execute_tool", file_glob: "**/.git/info/exclude" }
1227
+ ],
1228
+ none_of: [
1229
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1230
+ // the write tools, so an unknown file tool on a matched path still holds.
1231
+ { kind: "execute_tool", label: "{Read,Grep}" }
1176
1232
  ]
1177
1233
  },
1178
1234
  fixtures: {
@@ -1186,7 +1242,9 @@ var fsVcsInternals = {
1186
1242
  file(".gitignore"),
1187
1243
  file(".gitattributes"),
1188
1244
  file(".github/CODEOWNERS"),
1189
- file("src/index.ts")
1245
+ file("src/index.ts"),
1246
+ file(".git/config", "Read"),
1247
+ file(".git/hooks/pre-commit", "Grep")
1190
1248
  ]
1191
1249
  }
1192
1250
  };
@@ -1497,7 +1555,7 @@ var blockProdConfigEdit = {
1497
1555
  severity: "high",
1498
1556
  defaultAction: "require_approval",
1499
1557
  title: "Approve production config edits",
1500
- description: "Routes edits to production configuration files to human approval. Matched by path: `*.prod.*` and `*.production.*`, the bare `prod.*` / `production.*` spellings, and any file under a `prod/` or `production/` directory. Prose is excluded \u2014 `.md`, `.mdx` and `.txt` never match \u2014 so writing a runbook under `docs/production/` does not ask for approval to change production. It matches the PATH only: it cannot tell a real production config from a file that merely spells prod in its name, and it MISSES a production config named something else entirely, such as `values-live.yaml`.",
1558
+ description: "Routes edits to production configuration files to human approval. Matched by path: `*.prod.*` and `*.production.*`, the bare `prod.*` / `production.*` spellings, and any file under a `prod/` or `production/` directory. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so opening a production config to look at it does not ask for approval; every other file tool does, including one this corpus does not know. Prose is excluded too \u2014 `.md`, `.mdx` and `.txt` never match \u2014 so writing a runbook under `docs/production/` does not ask for approval to change production. It matches the PATH only: it cannot tell a real production config from a file that merely spells prod in its name, and it MISSES a production config named something else entirely, such as `values-live.yaml`.",
1501
1559
  match: {
1502
1560
  any_of: [
1503
1561
  { kind: "execute_tool", file_glob: "**/*.prod.*" },
@@ -1508,6 +1566,9 @@ var blockProdConfigEdit = {
1508
1566
  { kind: "execute_tool", file_glob: "**/production/**" }
1509
1567
  ],
1510
1568
  none_of: [
1569
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1570
+ // the write tools, so an unknown file tool on a matched path still holds.
1571
+ { kind: "execute_tool", label: "{Read,Grep}" },
1511
1572
  { kind: "execute_tool", file_glob: "**/*.md" },
1512
1573
  { kind: "execute_tool", file_glob: "**/*.mdx" },
1513
1574
  { kind: "execute_tool", file_glob: "**/*.txt" }
@@ -1525,7 +1586,9 @@ var blockProdConfigEdit = {
1525
1586
  file("docs/production/README.md"),
1526
1587
  file("config/database.dev.yml"),
1527
1588
  file("src/index.ts"),
1528
- file("package.json")
1589
+ file("package.json"),
1590
+ file("config/database.prod.yml", "Read"),
1591
+ file("k8s/production/deployment.yaml", "Grep")
1529
1592
  ]
1530
1593
  }
1531
1594
  };
@@ -2498,7 +2561,7 @@ var requireAuthOnPiiEndpoints = {
2498
2561
  severity: "medium",
2499
2562
  defaultAction: "require_approval",
2500
2563
  title: "Review API endpoint changes for auth",
2501
- description: "Routes edits to API route/handler SOURCE files to human approval so a reviewer can confirm authentication is present on new or changed endpoints. Matched by path: a source file under routes/, handlers/ or controllers/; an api/ directory nested inside a source tree (src/api/, app/api/, pages/api/); Next's route.ts convention; and the <name>.controller.* / <name>.routes.* spellings. HEURISTIC: a path signal only \u2014 it cannot inspect the edit for a missing auth check or exposed PII, so treat a match as confirm auth on this endpoint, not as a finding. It deliberately does NOT match every file in a package merely NAMED api, nor a client-side router table such as routes.tsx, which defines no endpoint; and it MISSES endpoints declared inline in a server file or by a framework convention not listed above.",
2564
+ description: "Routes edits to API route/handler SOURCE files to human approval so a reviewer can confirm authentication is present on new or changed endpoints. Matched by path: a source file under routes/, handlers/ or controllers/; an api/ directory nested inside a source tree (src/api/, app/api/, pages/api/); Next's route.ts convention; and the <name>.controller.* / <name>.routes.* spellings. Reading is excluded \u2014 the Read and Grep tools never match \u2014 so opening an endpoint's source to read it does not ask for approval; every other file tool does, including one this corpus does not know. HEURISTIC: a path signal only \u2014 it cannot inspect the edit for a missing auth check or exposed PII, so treat a match as confirm auth on this endpoint, not as a finding. It deliberately does NOT match every file in a package merely NAMED api, nor a client-side router table such as routes.tsx, which defines no endpoint; and it MISSES endpoints declared inline in a server file or by a framework convention not listed above.",
2502
2565
  match: {
2503
2566
  any_of: [
2504
2567
  { kind: "execute_tool", file_glob: `**/routes/**/*.${SOURCE}` },
@@ -2510,6 +2573,11 @@ var requireAuthOnPiiEndpoints = {
2510
2573
  { kind: "execute_tool", file_glob: `**/route.${SOURCE}` },
2511
2574
  { kind: "execute_tool", file_glob: `**/*.controller.${SOURCE}` },
2512
2575
  { kind: "execute_tool", file_glob: `**/*.routes.${SOURCE}` }
2576
+ ],
2577
+ none_of: [
2578
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
2579
+ // the write tools, so an unknown file tool on a matched path still holds.
2580
+ { kind: "execute_tool", label: "{Read,Grep}" }
2513
2581
  ]
2514
2582
  },
2515
2583
  fixtures: {
@@ -2523,7 +2591,9 @@ var requireAuthOnPiiEndpoints = {
2523
2591
  file("apps/api/README.md"),
2524
2592
  file("apps/api/package.json"),
2525
2593
  file("apps/api/src/lib/logger.ts"),
2526
- file("apps/web/src/app/routes.tsx")
2594
+ file("apps/web/src/app/routes.tsx"),
2595
+ file("src/api/users.ts", "Read"),
2596
+ file("src/controllers/payments.ts", "Grep")
2527
2597
  ]
2528
2598
  }
2529
2599
  };
@@ -3080,7 +3150,7 @@ var tiTestConfigEdit = {
3080
3150
  severity: "medium",
3081
3151
  defaultAction: "require_approval",
3082
3152
  title: "Editing a test runner or coverage configuration",
3083
- description: "Holds a file tool opening a test runner's or coverage tool's own configuration: Jest's `jest.config.*`, Vitest's `vitest.config.*` and legacy `vitest.workspace.*`, `pytest.ini`, `pytest.toml` and their dotted forms, `tox.ini`, Mocha's `.mocharc.*`, PHPUnit's `phpunit.xml`, `phpunit.xml.dist` and `phpunit.dist.xml`, `codecov.yml`, nyc's `.nycrc*` and `nyc.config.*`, `.c8rc`, coverage.py's `.coveragerc`, `karma.conf.*`, and the Playwright and Cypress configs. One line in any of these can exclude a failing file, lower a coverage threshold or retry a flaky test until it passes. File tools carry a path and no content, so it cannot tell a harmless edit from a weakening one, and it does not tell reading apart from editing. Deliberately NOT matched: general files that can also hold test settings \u2014 `pyproject.toml`, `setup.cfg`, `package.json`, `vite.config.*` \u2014 and test files themselves, since editing a test is how a test gets fixed. Misses test settings kept in those general files, and a config at a path passed with `--config`.",
3153
+ description: "Holds a file tool opening a test runner's or coverage tool's own configuration: Jest's `jest.config.*`, Vitest's `vitest.config.*` and legacy `vitest.workspace.*`, `pytest.ini`, `pytest.toml` and their dotted forms, `tox.ini`, Mocha's `.mocharc.*`, PHPUnit's `phpunit.xml`, `phpunit.xml.dist` and `phpunit.dist.xml`, `codecov.yml`, nyc's `.nycrc*` and `nyc.config.*`, `.c8rc`, coverage.py's `.coveragerc`, `karma.conf.*`, and the Playwright and Cypress configs. One line in any of these can exclude a failing file, lower a coverage threshold or retry a flaky test until it passes. Reading is excluded \u2014 the `Read` and `Grep` tools never match \u2014 so opening one of these configs to read it is not held; every other file tool is, including one this corpus does not know. File tools carry a path and no content, so it still cannot tell a harmless edit from a weakening one. Deliberately NOT matched: general files that can also hold test settings \u2014 `pyproject.toml`, `setup.cfg`, `package.json`, `vite.config.*` \u2014 and test files themselves, since editing a test is how a test gets fixed. Misses test settings kept in those general files, and a config at a path passed with `--config`.",
3084
3154
  match: {
3085
3155
  any_of: [
3086
3156
  { kind: "execute_tool", file_glob: "**/jest.config.{js,ts,mjs,mts,cjs,cts,json}" },
@@ -3105,6 +3175,11 @@ var tiTestConfigEdit = {
3105
3175
  kind: "execute_tool",
3106
3176
  file_glob: "**/{playwright,cypress}.config.{js,ts,mjs,mts,cjs,cts}"
3107
3177
  }
3178
+ ],
3179
+ none_of: [
3180
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
3181
+ // the write tools, so an unknown file tool on a matched path still holds.
3182
+ { kind: "execute_tool", label: "{Read,Grep}" }
3108
3183
  ]
3109
3184
  },
3110
3185
  fixtures: {
@@ -3137,7 +3212,9 @@ var tiTestConfigEdit = {
3137
3212
  file("jest.setup.js"),
3138
3213
  file(".env.test"),
3139
3214
  file("docs/testing.md"),
3140
- file("biome.json")
3215
+ file("biome.json"),
3216
+ file("vitest.config.ts", "Read"),
3217
+ file("jest.config.js", "Grep")
3141
3218
  ]
3142
3219
  }
3143
3220
  };
@@ -3623,8 +3700,8 @@ function isPack(value) {
3623
3700
  }
3624
3701
 
3625
3702
  // src/stamp.ts
3626
- var CATALOG_VERSION = "0.2.0";
3627
- var CATALOG_PUBLISHED_AT = "2026-09-18T13:00:33Z";
3703
+ var CATALOG_VERSION = "0.2.2";
3704
+ var CATALOG_PUBLISHED_AT = "2026-10-06T13:41:00Z";
3628
3705
 
3629
3706
  // src/rules.ts
3630
3707
  var RULES_BY_PACK = {