safe-chains 0.218.0

Auto-allow safe bash commands in agentic coding tools
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# Environment variables whose VALUE changes what a command does.
#
# `VAR=value cmd` is not `cmd`. A leading assignment can make the process run different code, load
# code from a different place, or reach a different system — all without changing the command name
# safe-chains classifies. This file is the researched list of variables where that is true, and the
# SHAPE of each one's value so a rule we already have can judge it.
#
# ONLY THE NAMES LISTED HERE ARE INSPECTED. An unlisted assignment is ignored exactly as before, and
# the absence of an assignment changes nothing. This file adds no denials outside itself — it is not
# a switch that makes unknown environment variables suspicious. That property is deliberate and is
# what makes the list safe to grow one entry at a time.
#
# Nothing here is "forbidden" either. `GIT_DIR=/tmp/evil` denies because `/tmp` is out-of-worktree
# under the ordinary locus rules, and `GIT_PAGER=cat` passes because `cat` passes. The value is
# classified; the name only says HOW.
#
# shape:
#   command    the value is a command line → classified by `command_verdict`, the same recursion
#              `sudo X` already uses. `GIT_PAGER=cat` allows, `GIT_PAGER='sh -c evil'` denies.
#   exec-path  the value names a path SUPPLYING CODE → gated at the executor locus, like cargo's
#              `--manifest-path`. Worktree allows; /tmp, home and system paths deny. This is not the
#              read rule: `cat /tmp/x.so` is a fine read, while LOADING /tmp/x.so is not.
#   data-path  the value names a path read or written as DATA → ordinary read/write locus.
#
# An entry may also carry `twin_flag` / `twin_base`, naming the FLAG spelling of the same thing:
#   twin_flag = "rsync -e {v} ./src/ ./dst/"   the flag invocation, `{v}` marking the value
#   twin_base = "rsync ./src/ ./dst/"          the command the variable prefixes
# `a_tagged_env_var_classifies_the_same_as_its_flag_twin` then holds the two spellings to the SAME
# verdict for a foreign path, a workspace path and a bare name. Tag every pair: gating one spelling
# and leaving the other open is a complete bypass that reads as a closed gate — which is exactly how
# `borg --rsh /tmp/evil` came to deny while `BORG_RSH=/tmp/evil borg` was approved.

# Every entry's `because` is the measured behaviour where one was taken; see
# docs/design/env-prefix-classification.md for the probes. Entries marked `measured = false` are
# enumerated from documentation because the toolchain was not installed to test against.

# ── Dynamic loader ───────────────────────────────────────────────────────────
# Injects code into the process before main. On macOS the DYLD_* pair is stripped by SIP for system
# binaries but honored for user-installed ones (Homebrew/cargo/npm) — which is most of a dev
# toolchain. LD_* is the Linux equivalent and is not restricted that way.
[env.LD_PRELOAD]
shape = "exec-path"
because = "loads an arbitrary shared object into the process before main"
[env.LD_AUDIT]
shape = "exec-path"
because = "loads an auditor library that observes and can alter symbol resolution"
[env.LD_LIBRARY_PATH]
shape = "exec-path"
because = "prepends a library search directory, so a planted .so satisfies a normal link"
[env.DYLD_INSERT_LIBRARIES]
shape = "exec-path"
because = "macOS loader injection; honored for user-installed binaries (measured)"
[env.DYLD_LIBRARY_PATH]
shape = "exec-path"
because = "macOS library search path"
[env.DYLD_FRAMEWORK_PATH]
shape = "exec-path"
because = "macOS framework search path"

# ── Shell ────────────────────────────────────────────────────────────────────
[env.BASH_ENV]
shape = "exec-path"
because = "sourced by non-interactive bash before the script runs (measured: executes)"
[env.ENV]
shape = "exec-path"
because = "the POSIX-sh equivalent of BASH_ENV"

# ── Command substitution: the value IS a command ─────────────────────────────
[env.GIT_SSH_COMMAND]
shape = "command"
because = "git runs this instead of ssh"
[env.GIT_SSH]
shape = "command"
because = "older form of GIT_SSH_COMMAND"
[env.GIT_EXTERNAL_DIFF]
shape = "command"
because = "git runs this instead of its own diff"
[env.GIT_EDITOR]
shape = "command"
because = "git launches this for interactive operations"
[env.GIT_SEQUENCE_EDITOR]
shape = "command"
because = "git launches this for the rebase todo list"
[env.GIT_PAGER]
shape = "command"
because = "git pipes output through this"
[env.GIT_ASKPASS]
shape = "command"
because = "git runs this to obtain credentials"
[env.GIT_PROXY_COMMAND]
shape = "command"
because = "git runs this to open a connection"
[env.EDITOR]
shape = "command"
because = "launched by many tools for interactive edits"
[env.VISUAL]
shape = "command"
because = "as EDITOR"
[env.PAGER]
shape = "command"
because = "output is piped through it"
[env.LESSOPEN]
shape = "command"
because = "less runs this input preprocessor on the file it is asked to show"
[env.LESSCLOSE]
shape = "command"
because = "less runs this after the preprocessor"
[env.RUSTC_WRAPPER]
shape = "command"
because = "cargo invokes this in place of rustc (measured: executes). The flag spelling, --config build.rustc-wrapper, is already denied"
[env.RUSTC_WORKSPACE_WRAPPER]
shape = "command"
because = "as RUSTC_WRAPPER, for workspace members"
[env.CARGO_BUILD_RUSTC_WRAPPER]
shape = "command"
because = "the CARGO_* config-mirror spelling of RUSTC_WRAPPER"
[env.RUSTC]
shape = "command"
because = "replaces the compiler cargo invokes"

# ── Code-supplying paths ─────────────────────────────────────────────────────
[env.PYTHONPATH]
shape = "exec-path"
because = "python auto-imports sitecustomize from this path at startup (measured: executes)"
[env.PYTHONHOME]
shape = "exec-path"
because = "relocates the interpreter's own standard library"
[env.PYTHONSTARTUP]
shape = "exec-path"
because = "executed at interactive interpreter start (measured: NOT executed for `python3 -c`)"
[env.RUBYLIB]
shape = "exec-path"
because = "ruby load path, so a planted file satisfies a later require"
[env.PERL5LIB]
shape = "exec-path"
because = "perl @INC, the lever that makes -M load attacker code"
[env.NODE_PATH]
shape = "exec-path"
because = "node module search path"
[env.NODE_REPL_EXTERNAL_MODULE]
shape = "exec-path"
because = "node loads this module in the REPL"
[env.CLASSPATH]
shape = "exec-path"
because = "JVM class search path (measured: a planted class on it was loaded and run)"
[env.DOTNET_STARTUP_HOOKS]
shape = "exec-path"
because = "the .NET host loads this assembly before main (measured: load attempted)"
[env.CORECLR_PROFILER_PATH]
shape = "exec-path"
measured = false
because = "CoreCLR loads this native profiler library"
[env.PHPRC]
shape = "exec-path"
because = "names a php.ini, which can set auto_prepend_file to execute code before every script (measured: executes)"
[env.PHP_INI_SCAN_DIR]
shape = "exec-path"
because = "a directory of additional php.ini files, same effect as PHPRC (measured: executes)"
[env.GIT_CONFIG_GLOBAL]
shape = "exec-path"
because = "git config can define aliases and core.pager, so a caller-controlled config file is code execution"
[env.GIT_CONFIG_SYSTEM]
shape = "exec-path"
because = "as GIT_CONFIG_GLOBAL"
[env.R_PROFILE_USER]
shape = "exec-path"
because = "R sources this at startup (measured: ran under Rscript)"

# ── Data paths ───────────────────────────────────────────────────────────────
# GIT_DIR and friends are exec-path, not data-path: a .git you point git at carries CONFIG, and git
# config defines aliases and core.pager. Pointing git at a foreign repo therefore runs foreign code —
# the same reasoning that makes cargo's --manifest-path an `exec` role rather than a read.
[env.GIT_DIR]
shape = "exec-path"
because = "selects which repository — and therefore whose config, aliases and pager — git obeys"
[env.GIT_WORK_TREE]
shape = "exec-path"
because = "retargets the working tree git acts on"
[env.GIT_INDEX_FILE]
shape = "data-path"
because = "retargets the index"
[env.GIT_OBJECT_DIRECTORY]
shape = "data-path"
because = "retargets object storage"
[env.GIT_ALTERNATE_OBJECT_DIRECTORIES]
shape = "data-path"
because = "additional object directories to read"
[env.GIT_COMMON_DIR]
shape = "exec-path"
because = "retargets the shared git dir, which holds config"

# ── Env twins of flags we already gate ───────────────────────────────────────
# A tool that mirrors its config surface into the environment gives every guarded flag a second
# spelling. Found by sweeping for "a flag we deny that has an environment form"; in each case the
# danger was ALREADY documented in the command's own TOML and only the spelling went unchecked.

# `cargo test --config target.<triple>.runner=X` is denied; the env spelling was not. The runner
# executes the compiled test binary, so it is a plain command substitution. The `*` stands for the
# target triple, which is why an exact-name table could not reach it.
[env."CARGO_TARGET_*_RUNNER"]
shape = "command"
because = "cargo runs this to execute the built test/bench binary; the --config spelling is denied"
[env."CARGO_TARGET_*_LINKER"]
shape = "command"
because = "cargo invokes this as the linker for that target"

# `git -c <key>=<value>` is already classified PER KEY, with `core.pager`'s value recursed as a
# command. The environment spelling splits one assignment across three variables
# (GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n), so no single value can be judged on
# its own — `GIT_CONFIG_VALUE_0` is a command when the paired key is `core.pager` and inert when it
# is `user.name`. Presence denies, which matches the flag spelling: `git -c user.name=x log` already
# denies because that key is not on git's permitted list.
[env.GIT_CONFIG_COUNT]
shape = "opaque"
because = "injects arbitrary git config; the key/value pairing is split across other variables, so a single value cannot be certified"
[env."GIT_CONFIG_KEY_*"]
shape = "opaque"
because = "as GIT_CONFIG_COUNT"
[env."GIT_CONFIG_VALUE_*"]
shape = "opaque"
because = "as GIT_CONFIG_COUNT"

# ── Interpreter option strings ───────────────────────────────────────────────
# These carry a FLAG STRING for an interpreter, so the value is parsed as flags and every token must
# match a researched-inert entry. The dangerous flags are never enumerated — a switch invented
# tomorrow denies by not being listed, which is what keeps these entries from rotting.
#
# A PATH-VALUED FLAG GOES IN `path_flags`, NEVER IN `allowed`. `allowed` matches the flag NAME and
# never looks at the value, so listing a path-valued flag there hands out an unclassified filesystem
# target: `-Cincremental` was in `allowed` and admitted `RUSTFLAGS='-Cincremental=/etc/cron.d' cargo
# build`, an out-of-worktree write, while the command-line spelling `cargo build --target-dir /etc/x`
# was denied — the env-twin-of-a-guarded-flag bug this file exists to close.
#
# `path_flags = [{ flag = "-Cincremental", role = "write" }]` instead routes the VALUE through the
# ordinary locus rules, so the env spelling and the flag spelling agree: in-worktree allows,
# out-of-worktree denies. `role` is read or write, spelled as the registry's `PathRole` spells it.
# An undeclared flag still denies, so this opens nothing by itself.
#
# AN ENTRY IS A WHOLE FLAG UNLESS IT ENDS IN `*`. `-mod` admits `-mod` and `-mod=vendor`, but NOT
# `-modfile=/tmp/evil.mod`, which is a different flag that merely starts the same way. Write the `*`
# only where the VALUE GLUES ONTO THE FLAG with no delimiter to stop it — `-Xmx*` for `-Xmx2g`, `-D*`
# for rustc's `-Dwarnings`. Every `*` widens the entry to things nobody researched, so it is a claim
# that nothing else in that tool shares the spelling; check before adding one.
#
# The accepted sets below were determined empirically (see the design note), not read from manuals:
# the Node documentation's claim that --require and --import are refused in NODE_OPTIONS is false on
# v22.17.0, where both load and execute.

# Ruby refuses -e itself. The remaining code vectors are -r<file> and -I<dir>, both paths, so they
# are excluded here and the invocation falls to approval rather than being path-classified — a
# RUBYOPT is not the place to be loading worktree code from.
[env.RUBYOPT]
shape = "option-string"
allowed = ["-w", "-W*", "-v", "--verbose", "--debug", "-E*", "-U", "-T*", "--jit", "--yjit",
           "--enable=*", "--disable=*"]
because = "ruby flag string; -r and -I load code (measured: -r executes)"

# Perl refuses -e/-E itself. -I is the lever that makes -M/-m/-d: load attacker code, so it is
# excluded; -M alone is confined to the installed @INC but is excluded too, since its danger is
# entirely contextual.
[env.PERL5OPT]
shape = "option-string"
allowed = ["-w", "-W", "-t", "-T", "-C*", "-D*"]
because = "perl flag string; -I/-M/-m/-d: load code (measured: -I -MInject and -I -d:Inj execute)"

# Node refuses only --eval/--print, so nearly everything else is accepted by the interpreter and the
# allowlist has to be narrow. Three distinct danger classes are excluded: code loading (--require,
# --import, --experimental-loader), a network-exposed debugger (--inspect*, which grants execution
# to anyone who can reach the port), and permission-model relaxers (--allow-*).
[env.NODE_OPTIONS]
shape = "option-string"
allowed = ["--max-old-space-size", "--max-semi-space-size", "--enable-source-maps",
           "--stack-size", "--no-warnings", "--no-deprecation", "--trace-warnings",
           "--trace-deprecation", "--unhandled-rejections", "--use-openssl-ca", "--use-bundled-ca",
           "--disable-warning", "--title", "--v8-pool-size"]
because = "node flag string; --require/--import execute (measured), --inspect exposes a debugger, --allow-* relax the permission model"

# `--config build.rustflags=[...]` is already denied; this is its environment twin. -C linker= and
# -C link-arg= invoke an arbitrary binary (measured: the linker ran), and -Z / --extern reach
# further, so only the diagnostic and codegen-tuning flags are listed.
[env.RUSTFLAGS]
shape = "option-string"
allowed = ["-Copt-level", "-Cdebuginfo", "-Cdebug-assertions", "-Coverflow-checks",
           "-Ctarget-cpu", "-Ctarget-feature", "-Ccodegen-units", "-Cpanic", "-Cstrip",
           "-Clto", "-D*", "-W*", "-A*", "-F*", "--cfg"]
path_flags = [{ flag = "-Cincremental", role = "write" }]
because = "rustc flag string; -C linker= runs an arbitrary binary (measured). The --config build.rustflags spelling is already denied"
[env.CARGO_BUILD_RUSTFLAGS]
shape = "option-string"
allowed = ["-Copt-level", "-Cdebuginfo", "-Cdebug-assertions", "-Coverflow-checks",
           "-Ctarget-cpu", "-Ctarget-feature", "-Ccodegen-units", "-Cpanic", "-Cstrip",
           "-Clto", "-D*", "-W*", "-A*", "-F*", "--cfg"]
path_flags = [{ flag = "-Cincremental", role = "write" }]
because = "the CARGO_* config-mirror spelling of RUSTFLAGS"
[env.RUSTDOCFLAGS]
shape = "option-string"
allowed = ["-D*", "-W*", "-A*", "-F*", "--cfg", "--document-private-items"]
because = "rustdoc flag string, same family as RUSTFLAGS"

# Applies to EVERY JVM started while set. Enumerated from the Oracle java launcher reference, then
# MEASURED on OpenJDK 26.0.2: all three variables were confirmed to execute a `-javaagent:` jar.
#
# `-XX:` CANNOT be a blanket prefix. Most -XX flags are GC/JIT tuning, but `-XX:OnError=<cmd>` and
# `-XX:OnOutOfMemoryError=<cmd>` run a custom command — direct execution wearing the same prefix as
# the harmless ones. BOTH were measured to run a script (OnOutOfMemoryError under a 16m heap;
# OnError under -XX:+CrashOnOutOfMemoryError). So the tuning flags are listed individually.
#
# `-XX:+ScavengeBeforeFullGC` is kept although OpenJDK 26 reports it "Unrecognized" — it dates to the
# ParallelGC era. An unrecognized VM option makes the JVM REFUSE TO START, so it cannot become a
# route to execution, and users on older JDKs still get the flag.
#
# `-D<name>=<value>` is NOT allowlisted, despite tuning guides calling system properties harmless.
# Properties reach security-relevant machinery (`java.rmi.server.codebase` historically enabled
# remote class loading, `jdk.attach.allowAttachSelf` opens self-attach), and telling which is which
# needs per-property research nobody has done.
#
# Excluded as code loaders: -javaagent:, -agentlib:, -agentpath:, -Xbootclasspath/a:, --module-path,
# --upgrade-module-path, @argfile, -XX:VMOptionsFile=, -XX:CompilerDirectivesFile=.
#
# -cp/-classpath/--class-path are NOT excluded — they are GATED, on JDK_JAVA_OPTIONS only. They are
# the flag spelling of CLASSPATH, which is already an `exec-path` entry, so refusing them here while
# admitting `CLASSPATH=./lib` was the same one-spelling-checked inconsistency this file exists to
# close; measured live on `mvn test` and `gradle build`, both of which auto-approve.
[env.JAVA_TOOL_OPTIONS]
shape = "option-string"
allowed = [
  "-Xms*", "-Xmx*", "-Xmn*", "-Xss*", "-verbose:*",
  "-XX:MaxDirectMemorySize", "-XX:ThreadStackSize", "-XX:MaxGCPauseMillis",
  "-XX:ReservedCodeCacheSize", "-XX:InitialCodeCacheSize", "-XX:CompileThresholdScaling",
  "-XX:+UseG1GC", "-XX:+UseSerialGC", "-XX:+UseParallelGC", "-XX:+UseZGC",
  "-XX:+ScavengeBeforeFullGC", "-XX:+UseStringDeduplication",
  "-XX:+TieredCompilation", "-XX:-TieredCompilation",
  "-XX:+PrintCommandLineFlags", "-XX:+PrintCompilation", "-XX:+HeapDumpOnOutOfMemoryError",
]
path_flags = [{ flag = "-XX:HeapDumpPath", role = "write" }]
because = "applies to every JVM started while set; -javaagent loads code and -XX:OnError/-XX:OnOutOfMemoryError run a command (all measured)"
[env._JAVA_OPTIONS]
shape = "option-string"
allowed = [
  "-Xms*", "-Xmx*", "-Xmn*", "-Xss*", "-verbose:*",
  "-XX:MaxDirectMemorySize", "-XX:ThreadStackSize", "-XX:MaxGCPauseMillis",
  "-XX:ReservedCodeCacheSize", "-XX:InitialCodeCacheSize", "-XX:CompileThresholdScaling",
  "-XX:+UseG1GC", "-XX:+UseSerialGC", "-XX:+UseParallelGC", "-XX:+UseZGC",
  "-XX:+ScavengeBeforeFullGC", "-XX:+UseStringDeduplication",
  "-XX:+TieredCompilation", "-XX:-TieredCompilation",
  "-XX:+PrintCommandLineFlags", "-XX:+PrintCompilation", "-XX:+HeapDumpOnOutOfMemoryError",
]
path_flags = [{ flag = "-XX:HeapDumpPath", role = "write" }]
because = "as JAVA_TOOL_OPTIONS; takes precedence over it (measured: -javaagent ran)"
[env.JDK_JAVA_OPTIONS]
shape = "option-string"
allowed = [
  "-Xms*", "-Xmx*", "-Xmn*", "-Xss*", "-verbose:*",
  "-XX:MaxDirectMemorySize", "-XX:ThreadStackSize", "-XX:MaxGCPauseMillis",
  "-XX:ReservedCodeCacheSize", "-XX:InitialCodeCacheSize", "-XX:CompileThresholdScaling",
  "-XX:+UseG1GC", "-XX:+UseSerialGC", "-XX:+UseParallelGC", "-XX:+UseZGC",
  "-XX:+ScavengeBeforeFullGC", "-XX:+UseStringDeduplication",
  "-XX:+TieredCompilation", "-XX:-TieredCompilation",
  "-XX:+PrintCommandLineFlags", "-XX:+PrintCompilation", "-XX:+HeapDumpOnOutOfMemoryError",
]
path_flags = [
  { flag = "-XX:HeapDumpPath", role = "write" },
  # LAUNCHER options, accepted here and NOWHERE ELSE (measured on OpenJDK 26.0.2: `-cp` in
  # JAVA_TOOL_OPTIONS/_JAVA_OPTIONS is "Unrecognized option" and the JVM refuses to start, while
  # here it loaded and ran a planted class). Gated at the executor locus, so they agree with the
  # CLASSPATH entry: `./lib` allows, `/tmp/evil` denies.
  { flag = "-cp", role = "exec", sep = "space" },
  { flag = "-classpath", role = "exec", sep = "space" },
  { flag = "--class-path", role = "exec", sep = "space" },
]
because = "the LAUNCHER-processed spelling: a superset of JAVA_TOOL_OPTIONS that also takes -cp (measured: -javaagent ran; -cp ran a planted class)"

# Go's default build flags, measured on go1.26.5. `-toolexec` invokes an arbitrary program in place
# of the toolchain — the direct analogue of RUSTC_WRAPPER — `-exec` runs the built binary through
# one, and `-overlay` maps disk paths to different backing files, substituting source before
# compilation. `-toolexec` and `-overlay` were both confirmed: the overlay probe printed the planted
# source, not the file on disk. `-ldflags`/`-gcflags` pass through to the linker and compiler and are
# excluded for the reason RUSTFLAGS' `-C link-arg` is.
#
# The whole-flag rule matters most here: `-mod` must not admit `-modfile`, which was measured to
# change the module the build resolves (`go list -m` reported the planted module name).
[env.GOFLAGS]
shape = "option-string"
allowed = ["-v", "-x", "-n", "-count", "-tags", "-race", "-mod", "-short", "-timeout",
           "-run", "-skip", "-buildvcs", "-trimpath", "-json"]
because = "-toolexec runs an arbitrary program and -overlay substitutes source files before compilation (both measured); -exec runs the built binary through one"

# LUA_INIT is code OR a path depending on its first character: a leading `@` names a file to run,
# anything else IS Lua source executed at interpreter start. Both forms were measured to run on Lua
# 5.5.0. One shape cannot express that, and Lua source is not something this classifier reads, so
# presence denies.
[env.LUA_INIT]
shape = "opaque"
because = "executes its contents as Lua, or runs the file named by a leading @, at interpreter start (measured: BOTH forms ran)"

[env.JULIA_LOAD_PATH]
shape = "exec-path"
because = "julia module search path (measured: a planted module satisfied a `using` and ran)"
[env.JULIA_DEPOT_PATH]
shape = "exec-path"
because = "the depot CONTAINS config/startup.jl, so pointing it anywhere runs that file at startup — no import needed (measured)"
[env.CORECLR_ENABLE_PROFILING]
shape = "opaque"
measured = false
because = "arms the CoreCLR profiler pair; the library it loads is named by CORECLR_PROFILER_PATH, so neither value certifies alone"

# ── Env twins of executor FLAGS ──────────────────────────────────────────────
# Each of these is the environment spelling of a flag that is already gated at the
# executor locus. Gating one spelling and not the other is not a partial fix: the
# ungated spelling is a complete bypass, and the flag gate reads as closed while it
# is not. `exec-path` (not `command`) so both spellings judge the value the same
# way — a bare name on $PATH stays trusted, a path into /tmp does not.
[env.BORG_RSH]
shape = "exec-path"
twin_flag = "borg --rsh {v} check repo"
twin_base = "borg check repo"
because = "borg runs this instead of ssh to reach the repository (the --rsh twin)"
[env.BORG_REMOTE_PATH]
shape = "exec-path"
twin_flag = "borg --remote-path {v} list repo"
twin_base = "borg list repo"
because = "the borg executable invoked on the far side (the --remote-path twin)"
[env.RESTIC_PASSWORD_COMMAND]
shape = "exec-path"
twin_flag = "restic --password-command {v} snapshots"
twin_base = "restic snapshots"
because = "restic runs this to obtain the repository password (the --password-command twin)"
[env.RSYNC_RSH]
shape = "exec-path"
twin_flag = "rsync -e {v} ./src/ ./dst/"
twin_base = "rsync ./src/ ./dst/"
because = "the remote shell rsync executes (the --rsh/-e twin)"
[env.KUBECONFIG]
shape = "exec-path"
because = "a kubeconfig can carry a users[].user.exec credential plugin the client RUNS (the --kubeconfig twin)"