@blogic-cz/agent-tools 1.14.2 → 1.14.4

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.
package/README.md CHANGED
@@ -730,6 +730,33 @@ Literal heredoc bodies can be written as data. Unquoted heredoc expansion is ana
730
730
 
731
731
  Copy operations in a Herdr chain require `credentialGuard.chainCopyDestinations`. Copies also receive this destination proof whenever the configured list is nonempty. The synchronous filesystem check refuses errors, symlinks, missing parent directories, and destinations outside the configured directories. Ordinary copies without a Herdr invocation or configured destinations still receive normal source and destination checks.
732
732
 
733
+ #### Agent instruction and configuration writes
734
+
735
+ The Bash `agent-config-write` rule refuses writes to these paths:
736
+
737
+ - Any file named `AGENTS.md`, `CLAUDE.md`, or `.mcp.json`, with case normalized.
738
+ - Project `.codex/` trees, `.claude/settings*.json`, and `.claude/commands/`, `.claude/agents/`, and `.claude/skills/` trees.
739
+ - Home `.codex/config.toml` and `.codex/prompts/`.
740
+ - Home `.claude.json`, `.claude/settings*.json`, and `.claude/commands/`, `.claude/agents/`, `.claude/skills/`, and `.claude/hooks/` trees.
741
+ - Home `.agents/skills/` and replacement of the home `.agents` directory itself.
742
+ - Replacement of a `.claude` directory itself.
743
+
744
+ The rule checks redirects, `tee`, in-place `sed`, copy and link destinations, and supported writer operands. These include `rsync`, `ditto`, `tmutil restore` destinations, archive output files and destinations, `git checkout` paths with or without `--`, `patch`, `truncate`, `touch`, and `wget` output paths. Without an output-document option, the `wget` URL basename supplies the output name. A glob that can match a protected name refuses. A bracket expression that the matcher cannot translate also refuses. A configured copy grant remains subject to the filesystem proof above.
745
+
746
+ Work data such as `~/.agents/jobs/`, `~/.claude/plans/`, and `~/.claude/projects/` remains writable. Ordinary document paths such as `README.md` remain writable. Reading an instruction file or copying it to `/tmp/x.md` can pass.
747
+
748
+ The guard treats an extensionless destination outside temporary roots as a possible directory and also checks that destination plus the source basename. The added candidate does not apply under `/tmp`, `/private/tmp`, `/var/folders`, or `/private/var/folders`. A literal local `TMPDIR` binding resolves through the same path checks. An unknown ambient `TMPDIR` still requires environment approval and a resolvable path. Explicit directory syntax retains its existing checks.
749
+
750
+ The check is static. It cannot establish whether a dotted destination is an existing directory. It does not inspect archive members, patch contents, remote transfers, source directory contents, or filesystem aliases. Named archive members such as `tar -xf x.tar AGENTS.md` also stay outside this check because the archive contents are unknown. Those contents can contain protected names that the command text does not name. General path checks do not resolve symlinks. The copy proof above is the bounded exception. PowerShell does not receive the Bash agent-configuration write rule.
751
+
752
+ #### Herdr launches
753
+
754
+ The `agent-launch-override` rule checks native arguments after `herdr agent start ... --`. It also checks modeled `tab create` and `pane split` forms that directly launch Codex or Claude. Global Herdr options do not skip the check. Prompt text stays data.
755
+
756
+ Codex accepts `-m` or `--model`, `--add-dir`, `--cd` or `-C`, and `--sandbox` or `-s` with `workspace-write` or `read-only`. Supported `-c` settings are `model_reasoning_effort`, `sandbox_workspace_write.network_access`, and the literal `shell_environment_policy.set.HERDR_*` values used by the supported launch form. Claude accepts `--model` and `--add-dir`. Both accept `--prompt` and one trailing prompt. Other native arguments refuse, including permission bypasses, arbitrary config overrides, and config profiles.
757
+
758
+ `herdr pane run`, `send-keys`, and `send-text` refuse with `no-analyzer` because the terminal state is unknown. The guard does not certify a running agent or an existing terminal.
759
+
733
760
  Sensitive paths include credential files and configured blocked paths. Known template paths and explicit allowed paths can provide exceptions. Process-environment files cannot be allowed by a path exception. Slash direction and Windows path case are normalized independently of the host OS. General path checks do not resolve filesystem symlinks or confine commands to the repository; the copy-destination check above is the bounded filesystem exception. Custom command patterns apply to the original text, nested shell text, and resolved invocations.
734
761
 
735
762
  The kubeconfig pattern protects path segments containing `kubeconfig`. A final ordinary Markdown basename such as `check-kubeconfig-topology.md` has a documentation exception; `kubeconfig.md` and kubeconfig-containing parent directories remain protected. Explicit allowed paths take precedence over ordinary blocked patterns. This filename convention does not attest file contents.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=backlog.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backlog.test.d.ts","sourceRoot":"","sources":["../../src/credential-guard/backlog.test.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../src/credential-guard/engine.test.ts"],"names":[],"mappings":"AA+aA,eAAO,MAAM,cAAc,UAoI1B,CAAC;AACF,eAAO,MAAM,gBAAgB,UA0T5B,CAAC;AA4FF,eAAO,MAAM,oBAAoB,UAShC,CAAC;AACF,eAAO,MAAM,sBAAsB,UA+BlC,CAAC"}
1
+ {"version":3,"file":"engine.test.d.ts","sourceRoot":"","sources":["../../src/credential-guard/engine.test.ts"],"names":[],"mappings":"AA+fA,eAAO,MAAM,cAAc,UAoI1B,CAAC;AACF,eAAO,MAAM,gBAAgB,UA0T5B,CAAC;AA4FF,eAAO,MAAM,oBAAoB,UAShC,CAAC;AACF,eAAO,MAAM,sBAAsB,UA+BlC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/credential-guard/policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAiB,UAAU,EAAsB,MAAM,EAAE,IAAI,EAAQ,MAAM,MAAM,CAAC;AAO9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAmB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAuB5D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAQ7D,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9F,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3E,CAAC;AACF,MAAM,MAAM,KAAK,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC/D;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAA;CAAE,CAAC;AAiBxD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,cAAc,QAAQ,CAAC;AAyHpC,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,QAAQ,CAs8CnF"}
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../../src/credential-guard/policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAiB,UAAU,EAAsB,MAAM,EAAE,IAAI,EAAQ,MAAM,MAAM,CAAC;AAO9F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,OAAO,KAAK,EAAmB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAuB5D,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAQ7D,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAC9F,MAAM,MAAM,QAAQ,GAChB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,GACnF;IAAE,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;CAAE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,KAAK,IAAI,CAAC;IAC5F,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IACnD,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACnD,QAAQ,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;CAC3E,CAAC;AACF,MAAM,MAAM,KAAK,GACb;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC/D;IACE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B,GACD;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAA;CAAE,CAAC;AAiBxD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AACzC,eAAO,MAAM,eAAe,IAAI,CAAC;AACjC,eAAO,MAAM,cAAc,QAAQ,CAAC;AA4JpC,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,QAAQ,CA67CnF"}
@@ -1,6 +1,6 @@
1
1
  import type { XargsInput } from "./xargs";
2
2
  export type Language = "bash" | "powershell" | "jq" | "sed" | "awk" | "runtime";
3
- export type OperandRole = "data" | "path" | "content" | "path-pattern" | "search-filter" | "names" | "env-name" | "script" | "test-filter" | "member" | "rg-type" | "write" | {
3
+ export type OperandRole = "data" | "path" | "content" | "path-pattern" | "search-filter" | "names" | "env-name" | "script" | "test-filter" | "member" | "rg-type" | "write" | "write-name" | {
4
4
  readonly code: Language;
5
5
  };
6
6
  export type Issue = {
@@ -15,6 +15,7 @@ export type Option = {
15
15
  };
16
16
  export type ExecutableEntry = {
17
17
  readonly name: string;
18
+ readonly rejectsAbbreviations?: boolean;
18
19
  readonly readsContent?: boolean;
19
20
  readonly readsStdinCode?: boolean;
20
21
  readonly builtinOnly?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/credential-guard/registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/credential-guard/registry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.14.2",
3
+ "version": "1.14.4",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure platform, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -0,0 +1,343 @@
1
+ import { expect, it } from "vitest";
2
+ import { parseBash } from "./bash";
3
+ import { judge } from "./policy";
4
+
5
+ const bash = (command: string) => judge(parseBash(command), { cwd: "/repo", home: "/users/test" });
6
+
7
+ it.each([
8
+ "tar -cf /tmp/o.tar -L ~/.ssh/id_rsa",
9
+ "tar -cf /tmp/o.tar -L ~/.ssh/id_rsa src",
10
+ "tar -cf /tmp/o.tar -I ~/.ssh/id_rsa",
11
+ "tar -cf /tmp/o.tar -I ~/.aws/credentials",
12
+ "tar -cf /tmp/o.tar -I ~/.ssh/id_rsa src",
13
+ "tar -cf /tmp/o.tar -b ~/.ssh/id_rsa",
14
+ "tar -cf /tmp/o.tar -s ~/.ssh/id_rsa",
15
+ "tar -cf /tmp/o.tar --exclude ~/.ssh/id_rsa src",
16
+ "tar -cf /tmp/o.tar --exclude=~/.ssh/id_rsa src",
17
+ "tar -cf/tmp/o.tar -I~/.ssh/id_rsa src",
18
+ "tar -cf /tmp/o.tar -W file=~/.ssh/id_rsa src",
19
+ ])("checks every tar value as a read: %s", (command) => {
20
+ expect(bash(command)).toMatchObject({
21
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
22
+ });
23
+ });
24
+
25
+ it.each([
26
+ "tar -c -Bf AGENTS.md src",
27
+ "tar -c -Sf AGENTS.md src",
28
+ "tar -c -wf AGENTS.md src",
29
+ "tar -r -Bf AGENTS.md src",
30
+ "tar --create -Sf AGENTS.md src",
31
+ "tar -Bf AGENTS.md -c src",
32
+ "tar Bf AGENTS.md -c src",
33
+ "tar -cLf AGENTS.md src",
34
+ "tar -cvLf AGENTS.md src",
35
+ "tar cLf AGENTS.md src",
36
+ "tar -cLf ~/.codex/config.toml src",
37
+ "tar -c -L -f AGENTS.md src",
38
+ "tar -c -W file=AGENTS.md src",
39
+ "tar -cW file=AGENTS.md src",
40
+ "tar -c -Wfile=AGENTS.md src",
41
+ "tar -cf /tmp/o.tar -W file=AGENTS.md src",
42
+ "tar -cf /tmp/x.tar -T AGENTS.md",
43
+ "tar -c -BfAGENTS.md src",
44
+ "tar -cf /tmp/o.tar --unknown=file=AGENTS.md src",
45
+ "tar -Af AGENTS.md src",
46
+ "tar Af AGENTS.md src",
47
+ "tar --catenate -f AGENTS.md src",
48
+ "tar --concatenate --file=AGENTS.md src",
49
+ "tar -W create -f AGENTS.md src",
50
+ "tar -W append -f AGENTS.md src",
51
+ "tar -W create -W file=AGENTS.md src",
52
+ "tar -f AGENTS.md -W create src",
53
+ "tar -W create -f ~/.codex/config.toml src",
54
+ "tar -W catenate -f AGENTS.md x.tar",
55
+ "tar -W concatenate -f AGENTS.md x.tar",
56
+ "tar -W update -f AGENTS.md src",
57
+ "tar -Wcreate -f AGENTS.md src",
58
+ ])("checks every tar value in whole-command write mode: %s", (command) => {
59
+ expect(bash(command)).toMatchObject({
60
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "agent-config-write" })]),
61
+ });
62
+ });
63
+
64
+ it("checks BSD tar -W write mode against startup files", () => {
65
+ expect(bash("tar -W create -f ~/.zshrc src")).toMatchObject({
66
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "startup-mutation" })]),
67
+ });
68
+ });
69
+
70
+ it.each([
71
+ ["tar -c -W file=/users/test/.zshrc src", "startup-mutation"],
72
+ ["tar -c -W file=/users/test/.ssh/config src", "sensitive-path"],
73
+ ["tar -xf x.tar --directory=/users/test/.zshrc", "startup-mutation"],
74
+ ["tar -xf x.tar -C /users/test/.ssh/config", "sensitive-path"],
75
+ ])("applies every tar destination rule: %s", (command, rule) => {
76
+ expect(bash(command)).toMatchObject({
77
+ findings: expect.arrayContaining([expect.objectContaining({ rule })]),
78
+ });
79
+ });
80
+
81
+ it.each(["tar -xf x.tar", "tar -tvf x.tar", "tar -Af /tmp/x.tar src", "tar -W verbose -xf x.tar"])(
82
+ "accepts ordinary tar modes: %s",
83
+ (command) => expect(bash(command)).toEqual({ kind: "pass" }),
84
+ );
85
+
86
+ it.each([
87
+ "tar -cf out.tar -T files.txt",
88
+ "tar -cf out.tar --files-from files.txt",
89
+ "tar -cf out.tar -X excl.txt src",
90
+ "tar -cvf out.tar -I zstd src",
91
+ "tar -cf out.tar -b 20 src",
92
+ "tar -cf out.tar -L 100 src",
93
+ "tar -cf /tmp/o.tar -n src",
94
+ "tar -cf /tmp/o.tar -l src",
95
+ "tar -cvf /tmp/o.tar -s /a/b/ src",
96
+ "tar -xf x.tar -O",
97
+ "tar -xOf x.tar",
98
+ "tar xOf x.tar",
99
+ "tar -xf x.tar -k",
100
+ "tar -xkf x.tar",
101
+ "tar xkf x.tar",
102
+ "tar -xmf x.tar",
103
+ "tar xmf x.tar",
104
+ "tar -xf x.tar -U",
105
+ "tar -xvf x.tar -W",
106
+ "tar -xf x.tar -s /a/b/",
107
+ "tar -cf out.tar --exclude build src",
108
+ "tar -cf out.tar -q src",
109
+ "tar -xqf x.tar",
110
+ "tar xqf x.tar",
111
+ ])("accepts tar options outside ambiguous write keys: %s", (command) => {
112
+ expect(bash(command)).toEqual({ kind: "pass" });
113
+ });
114
+
115
+ it.each([
116
+ "wget --execute=output_document=AGENTS.md https://example.com/x",
117
+ "wget --execute=output_document=/Users/test/.zshrc https://example.com/x",
118
+ "wget --execute=dir_prefix=/Users/test/.codex https://example.com/config.toml",
119
+ "wget --execute=input=urls.txt https://example.com/x",
120
+ "wget --config=/tmp/evil.rc https://example.com/x",
121
+ "wget --input-metalink=x.meta4",
122
+ "wget -e input=urls.txt",
123
+ "wget -e dir_prefix=/tmp https://example.com/x",
124
+ "wget -e output_document=AGENTS.md https://example.com/x",
125
+ "wget --execute output_document=AGENTS.md https://example.com/x",
126
+ "wget --config /tmp/evil.rc https://example.com/x",
127
+ "wget --input-metalink x.meta4",
128
+ "wget -eoutput_document=AGENTS.md https://example.com/x",
129
+ ])("refuses indirect wget output configuration: %s", (command) => {
130
+ expect(bash(command)).toMatchObject({
131
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "no-analyzer" })]),
132
+ });
133
+ });
134
+
135
+ it.each([
136
+ "printf x > ./AGENT?.md",
137
+ "printf x > ./AGENT[]S].md",
138
+ "printf x >> ./AGENT[]S].md",
139
+ "printf x > ./AGENT[S].md",
140
+ "echo x | tee /tmp/output > ./AGENT[]S].md",
141
+ "echo x | tee /tmp/output > ./AGENT?.md",
142
+ "cp /tmp/evil/AGENTS.md ~/repo",
143
+ "cp -R .claude ~/claude-backup",
144
+ "cp -R /tmp/evil/.codex ~/somedir",
145
+ "ln -s /tmp/evil/AGENTS.md /repo",
146
+ "cp -r /tmp/evil/.claude ~/repo",
147
+ "cp /tmp/evil/.mcp.json /repo",
148
+ "rsync /tmp/x ~/.codex/AGENTS.md",
149
+ "rsync -a /tmp/evil/ ~/.claude/",
150
+ "ditto /tmp/x ~/.codex/AGENTS.md",
151
+ "wget -O AGENTS.md https://example.com/x",
152
+ "tar -xf /tmp/x.tar -C ~/.claude",
153
+ "unzip /tmp/x.zip -d ~/.codex",
154
+ "git checkout /tmp/x -- AGENTS.md",
155
+ "patch AGENTS.md /tmp/x.patch",
156
+ "truncate -s 0 AGENTS.md",
157
+ "touch ~/.codex/AGENTS.md",
158
+ "touch AGENT?.md",
159
+ "touch ./AGENT[]S].md",
160
+ "sed --in-pl=bak -e s/a/b/ AGENTS.md",
161
+ "sed --in-pl AGENTS.md",
162
+ "sed --in-pl s/a/b/ AGENTS.md",
163
+ "sed --in s/a/b/ ~/.codex/AGENTS.md",
164
+ "sort --out=AGENTS.md /tmp/x",
165
+ "sort --outp=./AGENTS.md in.txt",
166
+ "base64 --out=AGENTS.md /tmp/x",
167
+ "tar --dir=~/.claude -xf /tmp/x.tar",
168
+ "tar --direc=~/.claude -xf /tmp/x.tar",
169
+ "patch --out=AGENTS.md public.txt /tmp/x.patch",
170
+ "wget --output-d=AGENTS.md https://example.com/x",
171
+ "wget --output-doc=AGENTS.md https://example.com/x",
172
+ "tmutil restore /Volumes/TM/x/AGENTS.md AGENTS.md",
173
+ "tmutil restore /Volumes/Backup/x ~/.claude/settings.json",
174
+ "tar -cf AGENTS.md /tmp/evil",
175
+ "tar --create --file=AGENTS.md /tmp/evil",
176
+ "tar -cv -f AGENTS.md /tmp/evil",
177
+ "tar --cre -f AGENTS.md /tmp/evil",
178
+ "wget https://example.com/AGENTS.md",
179
+ "wget --directory-prefix=~/.codex https://example.com/config.toml",
180
+ "wget -P ~/.codex https://example.com/config.toml",
181
+ "wget -P ~/.claude https://example.com/settings.json",
182
+ "tar czf ~/.claude/settings.json src",
183
+ "tar czf AGENTS.md src",
184
+ "tar -rf ~/.codex/config.toml src",
185
+ "tar -rf AGENTS.md x",
186
+ "tar -uf AGENTS.md src",
187
+ "tar -uf AGENTS.md x",
188
+ "tar --append -f AGENTS.md x",
189
+ "tar --append --file=AGENTS.md src",
190
+ "tar --app -f AGENTS.md src",
191
+ "tar --update -f AGENTS.md src",
192
+ "tar cfz AGENTS.md src",
193
+ "tar cpf ~/.claude/settings.json src",
194
+ "tar cpzf AGENTS.md src",
195
+ "tar caf AGENTS.md src",
196
+ "tar chf AGENTS.md src",
197
+ "tar cPf AGENTS.md src",
198
+ "tar -cpf AGENTS.md src",
199
+ "tar -caf AGENTS.md src",
200
+ "tar -cvpf AGENTS.md src",
201
+ "tar -cpzf ~/.codex/config.toml src",
202
+ "wget -P ~/.codex -i urls.txt",
203
+ "tar vczf AGENTS.md src",
204
+ "tar rf AGENTS.md src",
205
+ "tar uf AGENTS.md src",
206
+ "wget -a AGENTS.md https://example.com/x",
207
+ "wget --append-output=AGENTS.md https://example.com/x",
208
+ "git checkout HEAD AGENTS.md",
209
+ "git checkout -p HEAD ./AGENTS.md",
210
+ ])("refuses protected writes: %s", (command) => {
211
+ expect(bash(command)).toMatchObject({
212
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "agent-config-write" })]),
213
+ });
214
+ });
215
+
216
+ it.each([
217
+ "cp AGENTS.md /tmp/x.md",
218
+ "cp AGENTS.md ~/backup.md",
219
+ "printf x > ./REPORT?.md",
220
+ "cp -R .claude /tmp/claude-backup",
221
+ "cp AGENTS.md /private/tmp/backup",
222
+ "cp AGENTS.md /var/folders/test/backup",
223
+ "cp AGENTS.md /private/var/folders/test/backup",
224
+ 'TMPDIR=/private/tmp/job; cp AGENTS.md "$TMPDIR/backup"',
225
+ "rsync /tmp/x /tmp/y",
226
+ "rsync -e ssh -a src/ host:/srv/",
227
+ "rsync --rsh='ssh -p 2222' -a src/ host:/srv/",
228
+ "ditto /tmp/x /tmp/y",
229
+ "wget -O /tmp/x https://example.com/x",
230
+ "tar -xf /tmp/x.tar -C /tmp/out",
231
+ "tar -cf /tmp/out.tar public.txt",
232
+ "tar -xf/repo/.codex/config.toml",
233
+ "wget https://example.com/public.txt",
234
+ "wget -P downloads https://example.com/x",
235
+ "wget -P /tmp https://example.com/x",
236
+ "wget -t 3 https://example.com/x",
237
+ "wget -t 3 https://example.com/x.zip",
238
+ "wget --tries 3 https://example.com/x.zip",
239
+ "wget -T 10 https://example.com/x.zip",
240
+ "wget -U agent https://example.com/x.zip",
241
+ "wget --limit-rate 1m https://example.com/x.zip",
242
+ "wget -a log.txt https://example.com/x",
243
+ "wget --directory-prefix downloads --timeout 10 --user-agent agent --header 'Accept: text/plain' -l 2 -q -c -N -r -np -nd -nc https://example.com/x",
244
+ "tar czf /tmp/out.tar src",
245
+ "tar -czf /tmp/x.tgz src",
246
+ "tar -cpf /tmp/x.tar src",
247
+ "wget -i urls.txt -O /tmp/out",
248
+ "wget --input-file=urls.txt --output-document=/tmp/out",
249
+ "wget -P /tmp -i urls.txt",
250
+ "wget --directory-prefix=/private/tmp --input-file=urls.txt",
251
+ "wget -P /var/folders/job -i urls.txt",
252
+ 'TMPDIR=/private/tmp/job; wget -P "$TMPDIR" -i urls.txt',
253
+ "tar cfz /tmp/out.tar src",
254
+ "tar -rf /tmp/out.tar src",
255
+ "tar -uf /tmp/out.tar src",
256
+ "tar --append --file=/tmp/out.tar src",
257
+ "tar vczf /tmp/out.tar src",
258
+ "tar tf /tmp/out.tar",
259
+ "tar xf /tmp/out.tar",
260
+ "wget -O /tmp/x https://example.com/AGENTS.md",
261
+ "unzip /tmp/x.zip -d /tmp/out",
262
+ "git checkout main -- public.txt",
263
+ "git checkout main public.txt",
264
+ "patch public.txt /tmp/x.patch",
265
+ "truncate -s 0 public.txt",
266
+ "touch public.txt",
267
+ "touch .env",
268
+ "touch build/*.o",
269
+ "tmutil restore public /tmp/x",
270
+ "tmutil restore /Volumes/TM/a /tmp/x",
271
+ "tmutil restore",
272
+ ])("accepts ordinary operands: %s", (command) => {
273
+ expect(bash(command)).toEqual({ kind: "pass" });
274
+ });
275
+
276
+ it.each(["jq --rawf /tmp/x.txt file.txt", "jq --slurpf=/tmp/x.txt file.txt"])(
277
+ "keeps dangerous prefix fallback: %s",
278
+ (command) => {
279
+ expect(bash(command)).toMatchObject({
280
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "no-analyzer" })]),
281
+ });
282
+ },
283
+ );
284
+
285
+ it("keeps restore startup writes blocked", () => {
286
+ expect(bash("tmutil restore /Volumes/Backup/x ~/.zshrc")).toMatchObject({
287
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "startup-mutation" })]),
288
+ });
289
+ });
290
+
291
+ it.each([
292
+ "wget -i urls.txt",
293
+ "wget --input-file urls.txt",
294
+ "wget --input-file=urls.txt",
295
+ "wget -i -",
296
+ "wget -B https://example.com/ -i urls.txt",
297
+ "wget -P /tmp/../repo -i urls.txt",
298
+ "tar -cqf AGENTS.md src",
299
+ "tar cqf AGENTS.md src",
300
+ ])("refuses unknown output paths: %s", (command) => {
301
+ expect(bash(command)).toMatchObject({
302
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "no-analyzer" })]),
303
+ });
304
+ });
305
+
306
+ it.each([
307
+ "wget -P ~ https://example.com/.zshrc",
308
+ "wget -P ~ https://example.com/.profile",
309
+ "wget -P ~ https://example.com/.bashrc",
310
+ "wget -O ~/.zshrc https://example.com/x",
311
+ ])("refuses wget startup writes: %s", (command) => {
312
+ expect(bash(command)).toMatchObject({
313
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "startup-mutation" })]),
314
+ });
315
+ });
316
+
317
+ it.each([
318
+ "wget -P ~ https://example.com/.npmrc",
319
+ "wget -P ~/.ssh https://example.com/config",
320
+ "wget -P ~ https://example.com/.envrc",
321
+ ])("refuses wget sensitive destinations: %s", (command) => {
322
+ expect(bash(command)).toMatchObject({
323
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
324
+ });
325
+ });
326
+
327
+ it.each([
328
+ "rsync -e 'ssh -F ~/.ssh/config' src/ host:/srv/",
329
+ "rsync -e 'cat ~/.ssh/id_rsa' src/ host:/srv/",
330
+ ])("inspects remote shell arguments: %s", (command) => {
331
+ expect(bash(command)).toMatchObject({
332
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
333
+ });
334
+ });
335
+
336
+ it.each(["tmutil restore ~/.ssh/id_rsa /tmp/x", "tmutil restore /tmp/x ~/.aws/credentials"])(
337
+ "keeps sensitive restore refusal: %s",
338
+ (command) => {
339
+ expect(bash(command)).toMatchObject({
340
+ findings: expect.arrayContaining([expect.objectContaining({ rule: "sensitive-path" })]),
341
+ });
342
+ },
343
+ );
@@ -207,6 +207,86 @@ describe("agent configuration boundaries", () => {
207
207
  ]),
208
208
  });
209
209
  });
210
+ it.each([
211
+ "--remote unix:// --sandbox read-only",
212
+ "--sandbox read-only --remote unix://",
213
+ "--remote=unix:// --sandbox=read-only",
214
+ "--remote unix:// -s read-only",
215
+ "-sread-only --remote=unix://",
216
+ "-s=read-only --remote unix://",
217
+ "--remote unix:// -s read-only -m example -c model_reasoning_effort=low 'Read the brief'",
218
+ ])("allows local read-only remote launch %s", (args) => {
219
+ for (const prefix of ["", "rtk proxy "])
220
+ expect(
221
+ bash(`${prefix}herdr agent start worker --kind codex --pane w1:p2 -- ${args}`).kind,
222
+ ).toBe("pass");
223
+ });
224
+ it.each([
225
+ "--remote unix://",
226
+ "--remote",
227
+ "--remote unix:// --sandbox",
228
+ "--remote unix:// --sandbox workspace-write",
229
+ "--sandbox workspace-write --remote unix://",
230
+ "--remote unix:// -sworkspace-write",
231
+ "--remote unix:// --sandbox read-only -s read-only",
232
+ "--remote unix:// --sandbox read-only -s workspace-write",
233
+ "--remote unix:// --remote unix:// --sandbox read-only",
234
+ "--remote unix:// --remote=wss://example --sandbox read-only",
235
+ "--remote unix:// --sandbox read-only --add-dir /repo",
236
+ "--add-dir=/repo --sandbox read-only --remote unix://",
237
+ "--remote unix:// --sandbox read-only --yolo",
238
+ "--remote unix:// --sandbox read-only --dangerously-bypass-approvals-and-sandbox",
239
+ "--remote unix:// --sandbox read-only --dangerously-skip-permissions",
240
+ "--remote unix:// --sandbox read-only -c sandbox_mode=read-only",
241
+ "--remote unix:// --sandbox read-only -c approval_policy=never",
242
+ "--remote unix:// --sandbox read-only -c shell_environment_policy.inherit=all",
243
+ "--remote unix:// --sandbox read-only -c shell_environment_policy.exclude=[]",
244
+ "--remote unix:// --sandbox read-only --unknown-option value",
245
+ ])("refuses remote launch override %s even before a trailing prompt", (args) => {
246
+ for (const prompt of ["", " 'Read the brief'"])
247
+ expect(
248
+ bash(`rtk proxy herdr agent start worker --kind codex --pane w1:p2 -- ${args}${prompt}`),
249
+ ).toMatchObject({
250
+ findings: expect.arrayContaining([
251
+ expect.objectContaining({ rule: "agent-launch-override" }),
252
+ ]),
253
+ });
254
+ });
255
+ it.each([
256
+ "ws://localhost",
257
+ "wss://localhost",
258
+ "tcp://localhost:9000",
259
+ "/users/test/.codex/server.sock",
260
+ "unix:///users/test/.codex/server.sock",
261
+ "/users/test/.codex-other/server.sock",
262
+ "/users/test/.codex/../server.sock",
263
+ ".codex/server.sock",
264
+ "../.codex/server.sock",
265
+ "unix://relative.sock",
266
+ "unix:///tmp/server.sock",
267
+ "unix:///",
268
+ ])("refuses unproved remote endpoint %s", (endpoint) => {
269
+ expect(
270
+ bash(
271
+ `herdr agent start worker --kind codex --pane w1:p2 -- --sandbox read-only --remote ${endpoint}`,
272
+ ),
273
+ ).toMatchObject({
274
+ findings: expect.arrayContaining([
275
+ expect.objectContaining({ rule: "agent-launch-override" }),
276
+ ]),
277
+ });
278
+ });
279
+ it("keeps remote launch support exclusive to Codex", () => {
280
+ expect(
281
+ bash(
282
+ "herdr agent start worker --kind claude --pane w1:p2 -- --remote unix:// --sandbox read-only",
283
+ ),
284
+ ).toMatchObject({
285
+ findings: expect.arrayContaining([
286
+ expect.objectContaining({ rule: "agent-launch-override" }),
287
+ ]),
288
+ });
289
+ });
210
290
  it.each([
211
291
  "herdr tab create -- codex -c shell_environment_policy.inherit=all",
212
292
  "herdr pane split w1:p2 -- codex -c shell_environment_policy.inherit=all",
@@ -96,6 +96,41 @@ const glob = (text: string): Value => ({ ...opaque(text), globPatterns: [text] }
96
96
  function mentions(value: Value): readonly string[] {
97
97
  return value.kind === "literal" ? value.texts : value.kind === "opaque" ? value.mentions : [];
98
98
  }
99
+ function agentConfigGlob(value: Value): boolean {
100
+ return mentions(value).some((text) =>
101
+ text
102
+ .replace(/\\/g, "/")
103
+ .split("/")
104
+ .some((part) => {
105
+ if (!/[*?[]/.test(part)) return false;
106
+ if (part.replace(/\[[A-Za-z0-9._]\]/g, "").includes("[")) return true;
107
+ const pattern = new RegExp(
108
+ `^${part
109
+ .replace(/[.+^${}()|\\]/g, "\\$&")
110
+ .replace(/\[([A-Za-z0-9_]|\\\.)\]/g, "$1")
111
+ .replace(/\*/g, ".*")
112
+ .replace(/\?/g, ".")}$`,
113
+ "i",
114
+ );
115
+ return [
116
+ "AGENTS.md",
117
+ "CLAUDE.md",
118
+ ".mcp.json",
119
+ ".claude",
120
+ ".claude.json",
121
+ ".codex",
122
+ ".agents",
123
+ "config.toml",
124
+ "settings.json",
125
+ "settings.local.json",
126
+ "commands",
127
+ "agents",
128
+ "skills",
129
+ "hooks",
130
+ ].some((name) => pattern.test(name));
131
+ }),
132
+ );
133
+ }
99
134
  function join(a: Value, b: Value): Value {
100
135
  const texts = [...new Set([...mentions(a), ...mentions(b)])];
101
136
  return a.kind === "literal" && b.kind === "literal" && texts.length <= MAX_VALUE_ALTERNATIVES
@@ -251,7 +286,7 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
251
286
  path.startsWith(`${home}/.agents/skills/`);
252
287
  const agentConfigPath = (path: string): boolean =>
253
288
  agentConfigDirectory(path) ||
254
- /(?:^|\/)(?:agents\.md|claude\.md|\.mcp\.json|\.claude)$/.test(path) ||
289
+ /(?:^|\/)(?:agents\.md|claude\.md|\.mcp\.json|\.claude)\/?$/.test(path) ||
255
290
  /(?:^|\/)\.claude\/settings[^/]*\.json$/.test(path) ||
256
291
  path === `${home}/.agents` ||
257
292
  path === `${home}/.codex/config.toml` ||
@@ -527,7 +562,7 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
527
562
  stdin = target.kind === "stream" ? target.stdout : { ...opaque(...paths), supplied: true };
528
563
  }
529
564
  if (redirect.op !== "<") {
530
- if (current.dialect === "bash" && paths.some(agentConfigPath))
565
+ if (current.dialect === "bash" && (paths.some(agentConfigPath) || agentConfigGlob(target)))
531
566
  emit("agent-config-write", "write to agent instructions or configuration", span, true);
532
567
  outputs.push(
533
568
  ...paths.map((path) => ({
@@ -759,6 +794,24 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
759
794
  ? literal(operand.literal)
760
795
  : (values[operand.index] ?? opaque());
761
796
  const role = operand.role;
797
+ if (role === "write-name") {
798
+ if (
799
+ (value.kind !== "literal" && !(value.kind === "opaque" && value.globPatterns)) ||
800
+ invocationScope.cwd.kind !== "literal"
801
+ )
802
+ emit("opaque-path", "write target is not a known name or glob", span, true);
803
+ if (
804
+ scope.dialect === "bash" &&
805
+ (agentConfigGlob(value) ||
806
+ mentions(value).some((text) =>
807
+ mentions(invocationScope.cwd).some((cwd) =>
808
+ agentConfigPath(normalizePath(text, context.home, cwd)),
809
+ ),
810
+ ))
811
+ )
812
+ emit("agent-config-write", "write to agent instructions or configuration", span, true);
813
+ continue;
814
+ }
762
815
  if (typeof role !== "string") {
763
816
  const analyzedOutput = analyze(
764
817
  role.code,
@@ -841,39 +894,7 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
841
894
  scope.dialect === "bash" &&
842
895
  (role === "write" ||
843
896
  plan.directoryWrites.some((write) => write.sources.includes(operand.index))) &&
844
- mentions(value).some((text) =>
845
- text
846
- .replace(/\\/g, "/")
847
- .split("/")
848
- .some((part) => {
849
- if (!/[*?[]/.test(part)) return false;
850
- if (part.replace(/\[[A-Za-z0-9._]\]/g, "").includes("[")) return true;
851
- const pattern = new RegExp(
852
- `^${part
853
- .replace(/[.+^${}()|\\]/g, "\\$&")
854
- .replace(/\[([A-Za-z0-9_]|\\\.)\]/g, "$1")
855
- .replace(/\*/g, ".*")
856
- .replace(/\?/g, ".")}$`,
857
- "i",
858
- );
859
- return [
860
- "AGENTS.md",
861
- "CLAUDE.md",
862
- ".mcp.json",
863
- ".claude",
864
- ".claude.json",
865
- ".codex",
866
- ".agents",
867
- "config.toml",
868
- "settings.json",
869
- "settings.local.json",
870
- "commands",
871
- "agents",
872
- "skills",
873
- "hooks",
874
- ].some((name) => pattern.test(name));
875
- }),
876
- )
897
+ agentConfigGlob(value)
877
898
  )
878
899
  emit(
879
900
  "agent-config-write",
@@ -954,6 +975,8 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
954
975
  );
955
976
  const directory =
956
977
  destination.directory ||
978
+ (!/^\/(?:tmp|private\/tmp|(?:private\/)?var\/folders)(?:\/|$)/.test(path) &&
979
+ mentions(value).some((text) => !/\.[^./]+$/.test(text))) ||
957
980
  path === normalizePath(context.home, context.home, context.cwd) ||
958
981
  agentConfigDirectory(path) ||
959
982
  mentions(value).some((text) => /(?:\/|^\.\.?|\/\.\.?)$/.test(text));
@@ -961,7 +984,10 @@ export function judge(script: Script | ParseError, context: PolicyContext): Deci
961
984
  if (directory || plan.copyDestination) targets.push(target);
962
985
  }
963
986
  if (
964
- plan.copyDestination &&
987
+ (plan.copyDestination ||
988
+ executableName(first.texts[0] ?? "") === "wget" ||
989
+ executableName(first.texts[0] ?? "") === "tar" ||
990
+ (executableName(first.texts[0] ?? "") === "tmutil" && argv[1] === "restore")) &&
965
991
  targets.some((target) =>
966
992
  /(?:^|\/)(?:\.?(?:bashrc|bash_profile|profile|zshrc|zshenv|zprofile)|config\.fish)$/.test(
967
993
  target,
@@ -18,6 +18,7 @@ export type OperandRole =
18
18
  | "member"
19
19
  | "rg-type"
20
20
  | "write"
21
+ | "write-name"
21
22
  | { readonly code: Language };
22
23
  export type Issue = {
23
24
  readonly kind: "violation" | "missing-knowledge";
@@ -40,6 +41,7 @@ export type Option = {
40
41
  };
41
42
  export type ExecutableEntry = {
42
43
  readonly name: string;
44
+ readonly rejectsAbbreviations?: boolean;
43
45
  readonly readsContent?: boolean;
44
46
  readonly readsStdinCode?: boolean;
45
47
  readonly builtinOnly?: boolean;
@@ -1431,6 +1433,7 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
1431
1433
  },
1432
1434
  {
1433
1435
  name: "jq",
1436
+ rejectsAbbreviations: true,
1434
1437
  readsContent: true,
1435
1438
  startupFiles: [/(?:^|\/)\.jq$/],
1436
1439
  operands: [code("jq"), path],
@@ -1957,8 +1960,7 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
1957
1960
  name,
1958
1961
  {
1959
1962
  operands: [path],
1960
- effect:
1961
- name === "restore" ? unknown("unmodeled Time Machine restore operation") : "none",
1963
+ effect: "none",
1962
1964
  } satisfies Omit<ExecutableEntry, "name">,
1963
1965
  ]),
1964
1966
  ),
@@ -3091,6 +3093,137 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
3091
3093
  ),
3092
3094
  },
3093
3095
  },
3096
+ {
3097
+ name: "rsync",
3098
+ operands: [path],
3099
+ options: {
3100
+ ...flags("-a", "-r", "-v", "-z", "--archive", "--recursive", "--delete"),
3101
+ "-e": arg(code("bash")),
3102
+ "--rsh": arg(code("bash")),
3103
+ },
3104
+ },
3105
+ { name: "ditto", operands: [path], options: flags("-V", "-v", "--rsrc", "--norsrc") },
3106
+ {
3107
+ name: "tar",
3108
+ operands: [path],
3109
+ options: {
3110
+ ...flags(
3111
+ "-x",
3112
+ "--extract",
3113
+ "-c",
3114
+ "--create",
3115
+ "-r",
3116
+ "--append",
3117
+ "-u",
3118
+ "--update",
3119
+ "-A",
3120
+ "--catenate",
3121
+ "--concatenate",
3122
+ "-t",
3123
+ "--list",
3124
+ "-v",
3125
+ "-z",
3126
+ "-j",
3127
+ "-J",
3128
+ "-p",
3129
+ "-a",
3130
+ "-h",
3131
+ "-P",
3132
+ "-O",
3133
+ "--to-stdout",
3134
+ "-k",
3135
+ "-m",
3136
+ "-U",
3137
+ "-W",
3138
+ "-n",
3139
+ "-l",
3140
+ ),
3141
+ "-f": arg(path),
3142
+ "--file": arg(path),
3143
+ "-T": arg(path),
3144
+ "--files-from": arg(path),
3145
+ "-X": arg(path),
3146
+ "--exclude-from": arg(path),
3147
+ "--exclude": arg(data),
3148
+ "-b": arg(data),
3149
+ "-L": arg(data),
3150
+ "-I": arg(data),
3151
+ "-s": arg(data),
3152
+ "-C": arg("write"),
3153
+ "--directory": arg("write"),
3154
+ },
3155
+ },
3156
+ {
3157
+ name: "unzip",
3158
+ operands: [path],
3159
+ options: { ...flags("-q", "-o", "-n", "-l", "-t", "-v"), "-d": arg("write") },
3160
+ },
3161
+ {
3162
+ name: "patch",
3163
+ operands: ["write", path],
3164
+ options: {
3165
+ "-i": arg(path),
3166
+ "--input": arg(path),
3167
+ "-o": arg("write"),
3168
+ "--output": arg("write"),
3169
+ "-d": arg("write"),
3170
+ "--directory": arg("write"),
3171
+ "-p": arg(data),
3172
+ ...flags("-s", "-f", "-R", "--dry-run"),
3173
+ },
3174
+ },
3175
+ {
3176
+ name: "truncate",
3177
+ operands: ["write"],
3178
+ options: {
3179
+ "-s": arg(data),
3180
+ "--size": arg(data),
3181
+ "-r": arg(path),
3182
+ "--reference": arg(path),
3183
+ ...flags("-c", "--no-create"),
3184
+ },
3185
+ },
3186
+ {
3187
+ name: "touch",
3188
+ operands: ["write-name"],
3189
+ options: {
3190
+ "-t": arg(data),
3191
+ "-d": arg(data),
3192
+ "-r": arg(path),
3193
+ ...flags("-a", "-m", "-c", "-h", "--no-create"),
3194
+ },
3195
+ },
3196
+ {
3197
+ name: "wget",
3198
+ operands: [data],
3199
+ options: {
3200
+ "-O": arg("write"),
3201
+ "--output-document": arg("write"),
3202
+ "-o": arg("write"),
3203
+ "--output-file": arg("write"),
3204
+ "-a": arg("write"),
3205
+ "--append-output": arg("write"),
3206
+ "-P": arg("write"),
3207
+ "--directory-prefix": arg("write"),
3208
+ "-t": arg(data),
3209
+ "--tries": arg(data),
3210
+ "-T": arg(data),
3211
+ "--timeout": arg(data),
3212
+ "-U": arg(data),
3213
+ "--user-agent": arg(data),
3214
+ "--header": arg(data),
3215
+ "--limit-rate": arg(data),
3216
+ "-l": arg(data),
3217
+ "--level": arg(data),
3218
+ "-i": arg(path),
3219
+ "--input-file": arg(path),
3220
+ "-e": { roles: [data], issue: unknown("wget output configuration") },
3221
+ "--execute": { roles: [data], issue: unknown("wget output configuration") },
3222
+ "--config": { roles: [path], issue: unknown("wget output configuration") },
3223
+ "--input-metalink": { roles: [path], issue: unknown("wget output configuration") },
3224
+ ...flags("-q", "-c", "-nv", "-N", "-r", "-np", "-nd", "-nc"),
3225
+ },
3226
+ },
3094
3227
  { name: "seq", operands: [data], semantics: "seq" },
3095
3228
  { name: "tr", operands: [data], stdout: "opaque", options: flags("-c", "-C", "-d", "-s", "-t") },
3096
3229
  {
@@ -3116,7 +3249,7 @@ export const EXECUTABLES: readonly ExecutableEntry[] = [
3116
3249
  },
3117
3250
  {
3118
3251
  name: "rm",
3119
- aliases: ["touch", "du", "stat", "chown", "df"],
3252
+ aliases: ["du", "stat", "chown", "df"],
3120
3253
  options: {
3121
3254
  ...flags("-f", "-r", "-R", "-v", "-n", "-i", "-p", "-a", "-m", "-c", "-h", "-s"),
3122
3255
  "-t": arg(data),
@@ -3521,6 +3654,9 @@ function classifyInvocation(
3521
3654
  let codePositionals: number[] = [];
3522
3655
  const directoryWrites: Array<InvocationPlan["directoryWrites"][number]> = [];
3523
3656
  const writerCandidates: { index: number; literal: string }[] = [];
3657
+ let wgetOutputDocument = false;
3658
+ let wgetInputFile = false;
3659
+ let wgetDirectoryPrefix: string | undefined = ".";
3524
3660
  let inPlaceBackupSuffix: string | null = null;
3525
3661
  let sedEmptySuffix = false;
3526
3662
  let maxArgs: number | null = null;
@@ -4065,6 +4201,31 @@ function classifyInvocation(
4065
4201
  }
4066
4202
  return result();
4067
4203
  }
4204
+ const writesArchive =
4205
+ entry.name === "tar" &&
4206
+ argv.slice(1).some((word, index) => {
4207
+ if (word === undefined) return false;
4208
+ const longOption =
4209
+ word === "-W"
4210
+ ? argv[index + 2]?.split("=", 1)[0]
4211
+ : word.startsWith("-W")
4212
+ ? word.slice(2).split("=", 1)[0]
4213
+ : undefined;
4214
+ if (
4215
+ longOption &&
4216
+ ["create", "append", "update", "catenate", "concatenate"].some((option) =>
4217
+ option.startsWith(longOption),
4218
+ )
4219
+ )
4220
+ return true;
4221
+ if (word.startsWith("--"))
4222
+ return ["--create", "--append", "--update", "--catenate", "--concatenate"].some(
4223
+ (option) => word.length > 2 && option.startsWith(word.split("=")[0] ?? word),
4224
+ );
4225
+ const cluster =
4226
+ /^-([A-Za-z]+)/.exec(word)?.[1] ?? (index === 0 && /^[A-Za-z]+$/.test(word) ? word : "");
4227
+ return /[cruA]/.test(cluster);
4228
+ });
4068
4229
  while (position < argv.length) {
4069
4230
  if (skippedValues.has(position)) {
4070
4231
  position++;
@@ -4147,6 +4308,25 @@ function classifyInvocation(
4147
4308
  ...mode.options,
4148
4309
  }
4149
4310
  : { ...entry.options, ...mode.options };
4311
+ if (
4312
+ entry.name === "tar" &&
4313
+ position === 1 &&
4314
+ !endOptions &&
4315
+ /^[A-Za-z]+$/.test(a) &&
4316
+ [...a].some((letter) => "cxtru".includes(letter))
4317
+ ) {
4318
+ for (const letter of a) {
4319
+ if (!options[`-${letter}`] && writesArchive && a.includes("f"))
4320
+ issues.push(unknown(`unclassified tar option -${letter}`));
4321
+ for (const role of options[`-${letter}`]?.roles ?? []) {
4322
+ const index = ++position;
4323
+ if (index >= argv.length) issues.push(unknown(`missing -${letter} argument`));
4324
+ optionRoles.push({ index, role });
4325
+ }
4326
+ }
4327
+ position++;
4328
+ continue;
4329
+ }
4150
4330
  if (entry.semantics === "sed" && !endOptions && /^-[^-]/.test(a)) {
4151
4331
  const inPlaceIndex = a.indexOf("i", 1);
4152
4332
  if (
@@ -4180,7 +4360,9 @@ function classifyInvocation(
4180
4360
  if (!endOptions && !(optionName in options) && /^-[^-].+/.test(a)) {
4181
4361
  let offset = 1;
4182
4362
  const directoryWriter =
4183
- entry.semantics === "copy" || ["mv", "install", "ln"].includes(entry.name);
4363
+ entry.semantics === "copy" ||
4364
+ ["mv", "install", "ln"].includes(entry.name) ||
4365
+ (entry.name === "tar" && writesArchive && a.includes("f"));
4184
4366
  while (offset < a.length) {
4185
4367
  const option = options[`-${a[offset]}`];
4186
4368
  if (
@@ -4232,7 +4414,7 @@ function classifyInvocation(
4232
4414
  : [];
4233
4415
  const option =
4234
4416
  options[optionName] ??
4235
- (longOptions.length === 1 ? longOptions[0]?.[1] : undefined) ??
4417
+ (!entry.rejectsAbbreviations && longOptions.length === 1 ? longOptions[0]?.[1] : undefined) ??
4236
4418
  longOptions.find(
4237
4419
  ([, candidate]) =>
4238
4420
  candidate.issue || candidate.roles.some((role) => typeof role !== "string"),
@@ -4242,6 +4424,8 @@ function classifyInvocation(
4242
4424
  ([key]) => key.toLowerCase() === optionName.toLowerCase(),
4243
4425
  )?.[1]
4244
4426
  : undefined);
4427
+ if (option && !entry.rejectsAbbreviations && longOptions.length === 1 && !options[optionName])
4428
+ optionName = longOptions[0]?.[0] ?? optionName;
4245
4429
  if (
4246
4430
  !endOptions &&
4247
4431
  optionName.startsWith("--") &&
@@ -4265,6 +4449,10 @@ function classifyInvocation(
4265
4449
  issues.push(unknown("git config override"));
4266
4450
  }
4267
4451
  if (!endOptions && option && a.startsWith("-")) {
4452
+ if (entry.name === "wget" && ["-O", "--output-document"].includes(optionName))
4453
+ wgetOutputDocument = true;
4454
+ if (entry.name === "wget" && ["-i", "--input-file"].includes(optionName))
4455
+ wgetInputFile = true;
4268
4456
  if (
4269
4457
  entry.name === "bun" &&
4270
4458
  ["--filter", "--cwd"].includes(optionName) &&
@@ -4287,6 +4475,8 @@ function classifyInvocation(
4287
4475
  for (let i = 0; i < option.roles.length; i++) {
4288
4476
  const role = option.roles[i] ?? data;
4289
4477
  const index = i === 0 && attached !== undefined ? position : ++position;
4478
+ if (entry.name === "wget" && ["-P", "--directory-prefix"].includes(optionName))
4479
+ wgetDirectoryPrefix = attached ?? argv[index];
4290
4480
  if (
4291
4481
  index >= argv.length &&
4292
4482
  !(
@@ -4310,7 +4500,13 @@ function classifyInvocation(
4310
4500
  optionRoles.push({
4311
4501
  index,
4312
4502
  role,
4313
- ...(i === 0 && attached !== undefined ? { literal: attached } : {}),
4503
+ ...(entry.name === "rsync" &&
4504
+ typeof role !== "string" &&
4505
+ (attached ?? argv[index]) !== undefined
4506
+ ? { literal: `${attached ?? argv[index]} guard.invalid` }
4507
+ : i === 0 && attached !== undefined
4508
+ ? { literal: attached }
4509
+ : {}),
4314
4510
  });
4315
4511
  if (entry.semantics === "python" && optionName === "-m") {
4316
4512
  pythonModule = { index, name: attached ?? argv[index] };
@@ -4614,7 +4810,7 @@ function classifyInvocation(
4614
4810
  }
4615
4811
  if (inPlaceBackupSuffix !== null) {
4616
4812
  for (const operand of roles) if (operand.role === "path") operand.role = "write";
4617
- if (sedEmptySuffix && !filterOption && positional[0] !== undefined)
4813
+ if ((sedEmptySuffix || positional.length === 1) && !filterOption && positional[0] !== undefined)
4618
4814
  roles.push({ index: positional[0], role: "write" });
4619
4815
  }
4620
4816
  if (entry.name === "install") {
@@ -4630,7 +4826,74 @@ function classifyInvocation(
4630
4826
  if (operand.role === "write")
4631
4827
  directoryWrites.push({ index: operand.index, sources: candidates, directory: true });
4632
4828
  }
4633
- if (entry.semantics === "copy" || ["mv", "ln"].includes(entry.name)) {
4829
+ if (entry.name === "git" && mode === entry.subcommands?.checkout) {
4830
+ const separator = argv.indexOf("--");
4831
+ for (const operand of roles)
4832
+ if (separator >= 0 ? operand.index > separator : positional.slice(1).includes(operand.index))
4833
+ operand.role = "write";
4834
+ }
4835
+ if (entry.name === "tar") {
4836
+ const candidates = optionRoles.map(({ index, literal }) => ({ index, literal }));
4837
+ for (let index = 1; index < argv.length; index++) {
4838
+ const word = argv[index];
4839
+ if (word === undefined || !/^--?[A-Za-z0-9-]+$/.test(word)) {
4840
+ candidates.push({ index, literal: word });
4841
+ if (word?.startsWith("-") && !word.startsWith("--")) {
4842
+ for (
4843
+ let offset = 2;
4844
+ offset < word.length && /[A-Za-z]/.test(word[offset - 1] ?? "");
4845
+ offset++
4846
+ )
4847
+ candidates.push({ index, literal: word.slice(offset) });
4848
+ candidates.push({ index, literal: word.slice(1) });
4849
+ }
4850
+ }
4851
+ }
4852
+ for (const candidate of candidates) {
4853
+ roles.push({ ...candidate, role: path });
4854
+ if (writesArchive) roles.push({ ...candidate, role: "write" });
4855
+ const word = candidate.literal ?? argv[candidate.index];
4856
+ if (word !== undefined) {
4857
+ for (let equal = word.indexOf("="); equal >= 0; equal = word.indexOf("=", equal + 1)) {
4858
+ const value = { index: candidate.index, literal: word.slice(equal + 1) };
4859
+ roles.push({ ...value, role: path });
4860
+ if (writesArchive) roles.push({ ...value, role: "write" });
4861
+ }
4862
+ }
4863
+ }
4864
+ }
4865
+ if (entry.name === "wget" && !wgetOutputDocument) {
4866
+ if (
4867
+ wgetInputFile &&
4868
+ (wgetDirectoryPrefix === undefined ||
4869
+ !/^\/(?:tmp|private\/tmp|(?:private\/)?var\/folders)(?:\/|$)/.test(wgetDirectoryPrefix) ||
4870
+ wgetDirectoryPrefix.split("/").includes(".."))
4871
+ )
4872
+ issues.push(unknown("wget input file output names are not known"));
4873
+ for (const index of positional) {
4874
+ const url = argv[index];
4875
+ if (url === undefined || wgetDirectoryPrefix === undefined)
4876
+ issues.push(unknown("wget output name is not known"));
4877
+ else {
4878
+ try {
4879
+ const basename = new URL(url).pathname.split("/").at(-1);
4880
+ if (basename)
4881
+ roles.push({
4882
+ index,
4883
+ role: "write",
4884
+ literal: `${wgetDirectoryPrefix}/${decodeURIComponent(basename)}`,
4885
+ });
4886
+ } catch {
4887
+ issues.push(unknown("wget output name is not known"));
4888
+ }
4889
+ }
4890
+ }
4891
+ }
4892
+ if (
4893
+ entry.semantics === "copy" ||
4894
+ ["mv", "ln", "rsync", "ditto"].includes(entry.name) ||
4895
+ (entry.name === "tmutil" && mode === entry.subcommands?.restore)
4896
+ ) {
4634
4897
  const targetDirectory = optionRoles.find((operand) => operand.role === "write");
4635
4898
  const destination = targetDirectory?.index ?? positional.at(-1);
4636
4899
  if (destination !== undefined) {
@@ -4693,13 +4956,16 @@ function classifyInvocation(
4693
4956
  const command = argv[commandIndex];
4694
4957
  const action = argv[commandIndex + 1];
4695
4958
  const checkAgentArguments = (kind: string | undefined, start: number): void => {
4959
+ const launchOptions = new Map<string, (string | undefined)[]>();
4696
4960
  for (let index = start; index < argv.length; index++) {
4697
4961
  const argument = argv[index] ?? "";
4698
4962
  if (index === argv.length - 1 && argv[index] !== undefined && !argument.startsWith("-"))
4699
- return;
4963
+ break;
4700
4964
  const attached = /^(--[^=]+)=(.*)$/.exec(argument) ?? /^(-[mcCs])=?(.+)$/.exec(argument);
4701
4965
  const option = attached?.[1] ?? argument;
4702
4966
  const value = attached ? attached[2] : argv[++index];
4967
+ const key = option === "-s" ? "--sandbox" : option;
4968
+ launchOptions.set(key, [...(launchOptions.get(key) ?? []), value]);
4703
4969
  const configuration =
4704
4970
  value !== undefined &&
4705
4971
  (/^model_reasoning_effort=(?:minimal|low|medium|high|xhigh|max)$/.test(value) ||
@@ -4715,6 +4981,7 @@ function classifyInvocation(
4715
4981
  ["codex", "claude"].includes(kind ?? "")) ||
4716
4982
  (kind === "codex" &&
4717
4983
  (["-m", "--cd", "-C"].includes(option) ||
4984
+ (option === "--remote" && value === "unix://") ||
4718
4985
  (["--sandbox", "-s"].includes(option) &&
4719
4986
  ["workspace-write", "read-only"].includes(value)) ||
4720
4987
  (option === "-c" && configuration))));
@@ -4727,6 +4994,18 @@ function classifyInvocation(
4727
4994
  return;
4728
4995
  }
4729
4996
  }
4997
+ if (
4998
+ launchOptions.has("--remote") &&
4999
+ (launchOptions.get("--remote")?.length !== 1 ||
5000
+ launchOptions.get("--sandbox")?.length !== 1 ||
5001
+ launchOptions.get("--sandbox")?.[0] !== "read-only" ||
5002
+ launchOptions.has("--add-dir"))
5003
+ )
5004
+ issues.push({
5005
+ kind: "missing-knowledge",
5006
+ rule: "agent-launch-override",
5007
+ detail: "remote agents require one local endpoint and one read-only sandbox",
5008
+ });
4730
5009
  };
4731
5010
  if (command === "agent" && action === "start" && separator >= 0) {
4732
5011
  let kind: string | undefined;