@agenttrail/guardrails 0.2.0 → 0.2.1

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.
@@ -156,7 +156,7 @@ var acInstructionFileEdit = {
156
156
  severity: "high",
157
157
  defaultAction: "require_approval",
158
158
  title: "The agent editing its standing instructions",
159
- 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.",
159
+ 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.",
160
160
  match: {
161
161
  any_of: [
162
162
  {
@@ -172,6 +172,11 @@ var acInstructionFileEdit = {
172
172
  { kind: "execute_tool", file_glob: "**/.github/copilot-instructions.md" },
173
173
  { kind: "execute_tool", file_glob: "**/.github/instructions/**/*.instructions.md" },
174
174
  { kind: "execute_tool", file_glob: "**/.aider.conf.yml" }
175
+ ],
176
+ none_of: [
177
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
178
+ // the write tools, so an unknown file tool on a matched path still holds.
179
+ { kind: "execute_tool", label: "{Read,Grep}" }
175
180
  ]
176
181
  },
177
182
  fixtures: {
@@ -206,7 +211,9 @@ var acInstructionFileEdit = {
206
211
  file(".github/workflows/ci.yml"),
207
212
  file(".cursor/rules/style.mdc"),
208
213
  file(".claude/settings.json"),
209
- file(".aider.chat.history.md")
214
+ file(".aider.chat.history.md"),
215
+ file("CLAUDE.md", "Read"),
216
+ file(".github/copilot-instructions.md", "Grep")
210
217
  ]
211
218
  }
212
219
  };
@@ -220,7 +227,7 @@ var acMcpServerAdd = {
220
227
  severity: "high",
221
228
  defaultAction: "require_approval",
222
229
  title: "Adding an MCP server to an agent",
223
- 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.",
230
+ 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.",
224
231
  match: {
225
232
  any_of: [
226
233
  {
@@ -235,7 +242,14 @@ var acMcpServerAdd = {
235
242
  // Cursor has no `mcp add` command; its servers are added by editing this file.
236
243
  { kind: "execute_tool", file_glob: "**/.cursor/mcp.json" }
237
244
  ],
238
- none_of: [...QUOTED_MENTION]
245
+ none_of: [
246
+ // A read is not an addition. This covers the `.cursor/mcp.json` arm — the
247
+ // shell arm already pins its own tool family. Named as a deny-list rather
248
+ // than an `any_of` over the write tools, so an unknown file tool on that
249
+ // path still holds.
250
+ { kind: "execute_tool", label: "{Read,Grep}" },
251
+ ...QUOTED_MENTION
252
+ ]
239
253
  },
240
254
  fixtures: {
241
255
  block: [
@@ -260,7 +274,9 @@ var acMcpServerAdd = {
260
274
  bash("npx @modelcontextprotocol/inspector"),
261
275
  file("config/mcp.json"),
262
276
  file(".cursor/rules/style.mdc"),
263
- bash("pnpm run test")
277
+ bash("pnpm run test"),
278
+ file(".cursor/mcp.json", "Read"),
279
+ file("/Users/dev/.cursor/mcp.json", "Grep")
264
280
  ]
265
281
  }
266
282
  };
@@ -272,7 +288,7 @@ var acMemoryStoreEdit = {
272
288
  severity: "high",
273
289
  defaultAction: "require_approval",
274
290
  title: "The agent editing its own memory",
275
- 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.",
291
+ 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.",
276
292
  match: {
277
293
  any_of: [
278
294
  { kind: "execute_tool", file_glob: "**/.claude/projects/*/memory/**" },
@@ -281,6 +297,11 @@ var acMemoryStoreEdit = {
281
297
  { kind: "execute_tool", file_glob: "**/.gemini/tmp/*/memory/**" },
282
298
  { kind: "execute_tool", file_glob: "**/.codeium/windsurf/memories/**" },
283
299
  { kind: "execute_tool", file_glob: "**/.cursor/memory/**" }
300
+ ],
301
+ none_of: [
302
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
303
+ // the write tools, so an unknown file tool on a matched path still holds.
304
+ { kind: "execute_tool", label: "{Read,Grep}" }
284
305
  ]
285
306
  },
286
307
  fixtures: {
@@ -304,7 +325,9 @@ var acMemoryStoreEdit = {
304
325
  file("packages/memory-store/src/index.ts"),
305
326
  file("/Users/dev/.claude/projects/-Users-dev-shop/transcript.jsonl"),
306
327
  file("/home/dev/.gemini/settings.json"),
307
- file(".claude/settings.json")
328
+ file(".claude/settings.json"),
329
+ file(".claude/agent-memory/reviewer/MEMORY.md", "Read"),
330
+ file("/home/dev/.codex/memories/project.md", "Grep")
308
331
  ]
309
332
  }
310
333
  };
@@ -374,7 +397,7 @@ var acSkillInstall = {
374
397
  severity: "high",
375
398
  defaultAction: "require_approval",
376
399
  title: "The agent installing a skill, command or sub-agent",
377
- 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.",
400
+ 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.",
378
401
  match: {
379
402
  any_of: [
380
403
  { kind: "execute_tool", file_glob: "**/.claude/{skills,commands,agents,output-styles}/**" },
@@ -386,6 +409,11 @@ var acSkillInstall = {
386
409
  { kind: "execute_tool", file_glob: "**/.codeium/windsurf/{skills,global_workflows}/**" },
387
410
  { kind: "execute_tool", file_glob: "**/.cline/skills/**" },
388
411
  { kind: "execute_tool", file_glob: "**/SKILL.md" }
412
+ ],
413
+ none_of: [
414
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
415
+ // the write tools, so an unknown file tool on a matched path still holds.
416
+ { kind: "execute_tool", label: "{Read,Grep}" }
389
417
  ]
390
418
  },
391
419
  fixtures: {
@@ -415,7 +443,9 @@ var acSkillInstall = {
415
443
  file(".claude/settings.json"),
416
444
  file(".github/workflows/release.yml"),
417
445
  file("scripts/release.sh"),
418
- file(".gemini/settings.json")
446
+ file(".gemini/settings.json"),
447
+ file(".claude/skills/deploy/SKILL.md", "Read"),
448
+ file(".claude/commands/deploy.md", "Grep")
419
449
  ]
420
450
  }
421
451
  };
@@ -1007,7 +1037,7 @@ var fsAgentSelfConfig = {
1007
1037
  severity: "high",
1008
1038
  defaultAction: "require_approval",
1009
1039
  title: "The agent editing its own configuration",
1010
- 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.",
1040
+ 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.",
1011
1041
  match: {
1012
1042
  any_of: [
1013
1043
  { kind: "execute_tool", file_glob: "**/.claude/settings*.json" },
@@ -1018,6 +1048,11 @@ var fsAgentSelfConfig = {
1018
1048
  { kind: "execute_tool", file_glob: "**/.codex/config.toml" },
1019
1049
  { kind: "execute_tool", file_glob: "**/.agenttrail/guard/config.json" },
1020
1050
  { kind: "execute_tool", file_glob: "**/.agenttrail/guard/guardrails.json" }
1051
+ ],
1052
+ none_of: [
1053
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1054
+ // the write tools, so an unknown file tool on a matched path still holds.
1055
+ { kind: "execute_tool", label: "{Read,Grep}" }
1021
1056
  ]
1022
1057
  },
1023
1058
  fixtures: {
@@ -1031,7 +1066,9 @@ var fsAgentSelfConfig = {
1031
1066
  file(".claude/commands/deploy.md"),
1032
1067
  file("src/index.ts"),
1033
1068
  file("package.json"),
1034
- file("README.md")
1069
+ file("README.md"),
1070
+ file(".claude/settings.json", "Read"),
1071
+ file(".mcp.json", "Grep")
1035
1072
  ]
1036
1073
  }
1037
1074
  };
@@ -1043,7 +1080,7 @@ var fsCiDefinition = {
1043
1080
  severity: "medium",
1044
1081
  defaultAction: "require_approval",
1045
1082
  title: "Editing the CI pipeline definition",
1046
- 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.",
1083
+ 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.",
1047
1084
  match: {
1048
1085
  any_of: [
1049
1086
  { kind: "execute_tool", file_glob: "**/.github/workflows/**" },
@@ -1054,6 +1091,11 @@ var fsCiDefinition = {
1054
1091
  { kind: "execute_tool", file_glob: "**/azure-pipelines.yml" },
1055
1092
  { kind: "execute_tool", file_glob: "**/.buildkite/**" },
1056
1093
  { kind: "execute_tool", file_glob: "**/bitbucket-pipelines.yml" }
1094
+ ],
1095
+ none_of: [
1096
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1097
+ // the write tools, so an unknown file tool on a matched path still holds.
1098
+ { kind: "execute_tool", label: "{Read,Grep}" }
1057
1099
  ]
1058
1100
  },
1059
1101
  fixtures: {
@@ -1067,7 +1109,9 @@ var fsCiDefinition = {
1067
1109
  file(".github/CODEOWNERS"),
1068
1110
  file(".github/PULL_REQUEST_TEMPLATE.md"),
1069
1111
  file("package.json"),
1070
- file("README.md")
1112
+ file("README.md"),
1113
+ file(".github/workflows/ci.yml", "Read"),
1114
+ file(".gitlab-ci.yml", "Grep")
1071
1115
  ]
1072
1116
  }
1073
1117
  };
@@ -1079,7 +1123,7 @@ var fsSystemPaths = {
1079
1123
  severity: "high",
1080
1124
  defaultAction: "require_approval",
1081
1125
  title: "Writing to a system directory",
1082
- 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.",
1126
+ 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.",
1083
1127
  match: {
1084
1128
  any_of: [
1085
1129
  { kind: "execute_tool", file_glob: "/etc/**" },
@@ -1091,6 +1135,11 @@ var fsSystemPaths = {
1091
1135
  { kind: "execute_tool", file_glob: "/System/**" },
1092
1136
  { kind: "execute_tool", file_glob: "/Library/LaunchDaemons/**" },
1093
1137
  { kind: "execute_tool", file_glob: "**/Windows/System32/**" }
1138
+ ],
1139
+ none_of: [
1140
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1141
+ // the write tools, so an unknown file tool on a matched path still holds.
1142
+ { kind: "execute_tool", label: "{Read,Grep}" }
1094
1143
  ]
1095
1144
  },
1096
1145
  fixtures: {
@@ -1104,7 +1153,9 @@ var fsSystemPaths = {
1104
1153
  file("src/index.ts"),
1105
1154
  file("/home/dev/project/src/main.rs"),
1106
1155
  file("/tmp/scratch.txt", "Write"),
1107
- file("docs/etc-notes.md")
1156
+ file("docs/etc-notes.md"),
1157
+ file("/etc/hosts", "Read"),
1158
+ file("/usr/local/bin/app", "Grep")
1108
1159
  ]
1109
1160
  }
1110
1161
  };
@@ -1116,7 +1167,7 @@ var fsVcsInternals = {
1116
1167
  severity: "high",
1117
1168
  defaultAction: "require_approval",
1118
1169
  title: "Editing git's internals directly",
1119
- 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.",
1170
+ 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.",
1120
1171
  match: {
1121
1172
  any_of: [
1122
1173
  { kind: "execute_tool", file_glob: "**/.git/config" },
@@ -1124,6 +1175,11 @@ var fsVcsInternals = {
1124
1175
  { kind: "execute_tool", file_glob: "**/.git/refs/**" },
1125
1176
  { kind: "execute_tool", file_glob: "**/.git/HEAD" },
1126
1177
  { kind: "execute_tool", file_glob: "**/.git/info/exclude" }
1178
+ ],
1179
+ none_of: [
1180
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1181
+ // the write tools, so an unknown file tool on a matched path still holds.
1182
+ { kind: "execute_tool", label: "{Read,Grep}" }
1127
1183
  ]
1128
1184
  },
1129
1185
  fixtures: {
@@ -1137,7 +1193,9 @@ var fsVcsInternals = {
1137
1193
  file(".gitignore"),
1138
1194
  file(".gitattributes"),
1139
1195
  file(".github/CODEOWNERS"),
1140
- file("src/index.ts")
1196
+ file("src/index.ts"),
1197
+ file(".git/config", "Read"),
1198
+ file(".git/hooks/pre-commit", "Grep")
1141
1199
  ]
1142
1200
  }
1143
1201
  };
@@ -1448,7 +1506,7 @@ var blockProdConfigEdit = {
1448
1506
  severity: "high",
1449
1507
  defaultAction: "require_approval",
1450
1508
  title: "Approve production config edits",
1451
- 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`.",
1509
+ 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`.",
1452
1510
  match: {
1453
1511
  any_of: [
1454
1512
  { kind: "execute_tool", file_glob: "**/*.prod.*" },
@@ -1459,6 +1517,9 @@ var blockProdConfigEdit = {
1459
1517
  { kind: "execute_tool", file_glob: "**/production/**" }
1460
1518
  ],
1461
1519
  none_of: [
1520
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
1521
+ // the write tools, so an unknown file tool on a matched path still holds.
1522
+ { kind: "execute_tool", label: "{Read,Grep}" },
1462
1523
  { kind: "execute_tool", file_glob: "**/*.md" },
1463
1524
  { kind: "execute_tool", file_glob: "**/*.mdx" },
1464
1525
  { kind: "execute_tool", file_glob: "**/*.txt" }
@@ -1476,7 +1537,9 @@ var blockProdConfigEdit = {
1476
1537
  file("docs/production/README.md"),
1477
1538
  file("config/database.dev.yml"),
1478
1539
  file("src/index.ts"),
1479
- file("package.json")
1540
+ file("package.json"),
1541
+ file("config/database.prod.yml", "Read"),
1542
+ file("k8s/production/deployment.yaml", "Grep")
1480
1543
  ]
1481
1544
  }
1482
1545
  };
@@ -2449,7 +2512,7 @@ var requireAuthOnPiiEndpoints = {
2449
2512
  severity: "medium",
2450
2513
  defaultAction: "require_approval",
2451
2514
  title: "Review API endpoint changes for auth",
2452
- 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.",
2515
+ 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.",
2453
2516
  match: {
2454
2517
  any_of: [
2455
2518
  { kind: "execute_tool", file_glob: `**/routes/**/*.${SOURCE}` },
@@ -2461,6 +2524,11 @@ var requireAuthOnPiiEndpoints = {
2461
2524
  { kind: "execute_tool", file_glob: `**/route.${SOURCE}` },
2462
2525
  { kind: "execute_tool", file_glob: `**/*.controller.${SOURCE}` },
2463
2526
  { kind: "execute_tool", file_glob: `**/*.routes.${SOURCE}` }
2527
+ ],
2528
+ none_of: [
2529
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
2530
+ // the write tools, so an unknown file tool on a matched path still holds.
2531
+ { kind: "execute_tool", label: "{Read,Grep}" }
2464
2532
  ]
2465
2533
  },
2466
2534
  fixtures: {
@@ -2474,7 +2542,9 @@ var requireAuthOnPiiEndpoints = {
2474
2542
  file("apps/api/README.md"),
2475
2543
  file("apps/api/package.json"),
2476
2544
  file("apps/api/src/lib/logger.ts"),
2477
- file("apps/web/src/app/routes.tsx")
2545
+ file("apps/web/src/app/routes.tsx"),
2546
+ file("src/api/users.ts", "Read"),
2547
+ file("src/controllers/payments.ts", "Grep")
2478
2548
  ]
2479
2549
  }
2480
2550
  };
@@ -3031,7 +3101,7 @@ var tiTestConfigEdit = {
3031
3101
  severity: "medium",
3032
3102
  defaultAction: "require_approval",
3033
3103
  title: "Editing a test runner or coverage configuration",
3034
- 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`.",
3104
+ 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`.",
3035
3105
  match: {
3036
3106
  any_of: [
3037
3107
  { kind: "execute_tool", file_glob: "**/jest.config.{js,ts,mjs,mts,cjs,cts,json}" },
@@ -3056,6 +3126,11 @@ var tiTestConfigEdit = {
3056
3126
  kind: "execute_tool",
3057
3127
  file_glob: "**/{playwright,cypress}.config.{js,ts,mjs,mts,cjs,cts}"
3058
3128
  }
3129
+ ],
3130
+ none_of: [
3131
+ // A read is not an edit. Named as a deny-list rather than an `any_of` over
3132
+ // the write tools, so an unknown file tool on a matched path still holds.
3133
+ { kind: "execute_tool", label: "{Read,Grep}" }
3059
3134
  ]
3060
3135
  },
3061
3136
  fixtures: {
@@ -3088,7 +3163,9 @@ var tiTestConfigEdit = {
3088
3163
  file("jest.setup.js"),
3089
3164
  file(".env.test"),
3090
3165
  file("docs/testing.md"),
3091
- file("biome.json")
3166
+ file("biome.json"),
3167
+ file("vitest.config.ts", "Read"),
3168
+ file("jest.config.js", "Grep")
3092
3169
  ]
3093
3170
  }
3094
3171
  };
@@ -3574,8 +3651,8 @@ function isPack(value) {
3574
3651
  }
3575
3652
 
3576
3653
  // src/stamp.ts
3577
- var CATALOG_VERSION = "0.2.0";
3578
- var CATALOG_PUBLISHED_AT = "2026-09-18T13:00:33Z";
3654
+ var CATALOG_VERSION = "0.2.1";
3655
+ var CATALOG_PUBLISHED_AT = "2026-09-29T03:49:22Z";
3579
3656
 
3580
3657
  // src/rules.ts
3581
3658
  var RULES_BY_PACK = {
@@ -3625,4 +3702,4 @@ export {
3625
3702
  getRule,
3626
3703
  rulesForPack
3627
3704
  };
3628
- //# sourceMappingURL=chunk-3NDBRXP3.js.map
3705
+ //# sourceMappingURL=chunk-SLWHBH7H.js.map