Skip to content

feat(rule): resolve a variable executable assigned in the same input - #53

Open
ronheichman wants to merge 4 commits into
perplexityai:mainfrom
ronheichman:resolve-variable-executable
Open

feat(rule): resolve a variable executable assigned in the same input#53
ronheichman wants to merge 4 commits into
perplexityai:mainfrom
ronheichman:resolve-variable-executable

Conversation

@ronheichman

@ronheichman ronheichman commented Sep 8, 2026

Copy link
Copy Markdown

What

A POSIX command whose first word is a variable now projects with the variable's value as its executable when the same input assigns that variable exactly once by a plain top-level NAME=value with a static one-word value before the use and writes variables no other way, with every other case unchanged and every disabling condition listed in docs/rules.md.

Why

GH=/usr/bin/gh; $GH repo fork x was dropped as a dynamic executable and passed a name-based enforce rule that denies gh repo fork x, and this closes that gap without emulating shell state (stacked on #46).

How we verified it

33 resolvable and 144 unresolvable test forms, 89 source mutations each killed by a test, sixteen adversarial review passes, a differential replay of 9,574 inputs against the previous build, race, shuffle, fuzz, the repository lint set, and a replay of 7,312 real agent commands with no finding or deny change against the #46 base.

Built with Claude Code

@ronheichman
ronheichman force-pushed the resolve-variable-executable branch 2 times, most recently from bea5a06 to e0e5a0e Compare September 8, 2026 14:46
A POSIX command whose first word is a variable is projected with the
variable's value as its executable when the same input assigned that
variable exactly once, by a plain top-level NAME=value statement with a
static one-word value, before the use, and the input writes variables no
other way. GH=/usr/bin/gh; $GH repo fork x then projects as a static
gh command, so a name-based enforce rule denies it. Every other run-time
first word keeps the current behavior: dropped with the diagnostic
"dynamic command executable".

The resolver walks the parsed input twice: it counts plain assignments
and loop bindings per variable, records the single top-level static
assignment per name, and gives up on the whole input when it sees a
write the count cannot follow: arithmetic, coproc, a {name}> redirect, a
zsh glob qualifier, a declaration with a non-option or rebinding
operand, a write to IFS, PS4, BASH_ENV, ENV, ZDOTDIR, or HOME,
printf -v, set -A, set -k, set -o keyword, shopt -o keyword, wait -p,
test -v on an array element, the builtins that read or evaluate into
variables (also behind command, builtin, noglob, nocorrect), or a
command whose first word is neither static nor resolved. Names the
shell manages, and values with whitespace, wildcards, ~, =, or $'...'
quoting, never resolve. A script passed to an interpreter resolves only
when the outer input resolves, the call sets no environment word, its
option words, script, and redirects are readable as written, and no
option selects keyword mode. The resolved word is substituted as a
literal into a shallow copy of the call before projection, so
projection, wrapper unwrapping, interpreter detection, and the compound
enforcement candidates see one static program.

Verified with 33 resolvable and 144 unresolvable forms, 89 source
mutations that each fail at least one test, sixteen independent review
passes, a differential replay of 9,574 inputs against the previous
build, race, shuffle, fuzz, the repository lint set, and a replay of
7,312 real commands with no finding or deny change against the base.

Built with Claude Code
@ronheichman
ronheichman force-pushed the resolve-variable-executable branch from e0e5a0e to 1bb0f23 Compare September 8, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant