feat: per-spawn permission evaluator (execve-time gating via bashlex decomposition) - #1242
Open
samidarko wants to merge 3 commits into
Open
feat: per-spawn permission evaluator (execve-time gating via bashlex decomposition)#1242samidarko wants to merge 3 commits into
samidarko wants to merge 3 commits into
Conversation
Add an opt-in permission evaluator that gates the Bash tool by decomposing the shell command into the individual processes it would spawn, rather than matching the raw command text with a regex or substring blocklist (which misclassifies compositional shell: pipes, &&/||, subshells, command substitution, and cd changing the working directory). The new claude_agent_sdk.shell_permissions module: - decompose(): parses a command with bashlex and walks it into a list of Spawn objects, tracking working-directory changes across the command with a cwd stack so path-relative checks stay correct. Any construct it cannot prove safe to decompose (heredocs, process substitution, backticks, arithmetic expansion, control flow, eval/source/exec) is denied fail-safe with a named reason. - evaluate(): runs each spawn through a caller-supplied per-binary safety function; a missing entry denies (all-must-pass, fail closed). - create_bash_permission_evaluator(): adapts the engine into a can_use_tool callback. It is opt-in via the CLAUDE_AGENT_SDK_SHELL_PERMISSIONS environment variable and defers to a fallback (or allows) when unset, so existing behavior is unchanged until explicitly enabled. bashlex is an optional dependency, declared under the new [shell-permissions] extra and imported lazily with a helpful error.
Add unit tests for both layers: the pure engine (decomposition of pipes, sequences, logical operators, redirects; fail-safe deny on unsupported constructs; cwd tracking across cd; per-binary safety functions and the all-must-pass rule) and the can_use_tool adapter (the opt-in env gate, allow/deny mapping onto the permission results, fail-safe deny surfaced to the caller, and deferral for non-shell tools). Example policies are synthetic and illustrative.
Add a README section and a runnable example showing how to build the per-spawn Bash permission evaluator, register per-binary safety functions, and enable it via the CLAUDE_AGENT_SDK_SHELL_PERMISSIONS environment variable.
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
I think this can fail open through shell mechanisms that never reach the policy. Redirects are skipped entirely, so cat x > sensitive or even > sensitive can be auto-approved, and assignments/state changes are also ignored, so PATH=/tmp/evil:$PATH grep ... is evaluated as the allowed binary grep even though Bash may execute /tmp/evil/grep. Because this is an auto-approval gate, could redirects and environment changes that affect executable resolution be modelled or fail closed?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
Bashtool runs a full shell command, which can be compositional: pipes,&&/||sequences, subshells, command substitution, andcdchanging theworking directory mid-command. Classifying such a command by matching the raw
text with a regex or a substring blocklist produces both false positives (a
safe
grep ... | awk '$1 > N'denied because it "looks" conditional) and falsenegatives (a dangerous binary hidden past a pipe or a
cd). A robust gate needsto decompose the command into the individual processes it would actually spawn
and reason about each one.
Design
New opt-in module
claude_agent_sdk.shell_permissions:decompose(command, initial_cwd=None)parses the command withbashlexand walks the AST into a listof
Spawn(binary, argv, cwd)objects, one perexecve-level processinvocation. A
cwdstack tracks working-directory changes across thecommand (
cd /etc && cat passwdrecordscatwithcwd=/etc), and honorsbash scoping — a
cdinside a pipe segment or( … )subshell does notescape. Pipes, sequences (
;,&, newline), logical operators, simpleredirects, group commands
{ … }/( … ), and bounded command substitutionare supported.
named reason instead of a spawn list: heredocs/here-strings, process
substitution, backticks, arithmetic expansion, control flow
(
if/for/while/case/select/until/functions), the dynamic-executionbuiltins (
eval,source,.,exec), command substitution nested pastMAX_SUBSTITUTION_DEPTH, acdthat needs runtime state (cd,cd -,cd ~,cd $VAR), a bashlex parse error, and any unrecognized node kind. Itnever silently allows.
evaluate(command, initial_cwd=None, policy=None)runs each spawn througha caller-supplied per-binary safety function
fn(argv, cwd) -> bool. A binarywith no registered function denies (all-must-pass, fail closed). The engine
ships an empty
DEFAULT_POLICY, so an unconfigured evaluator denieseverything.
create_bash_permission_evaluator(policy, *, tool_name="Bash", command_key="command", env_var="CLAUDE_AGENT_SDK_SHELL_PERMISSIONS", initial_cwd=None, fallback=None)adapts the engine into acan_use_toolcallback that returns
PermissionResultAllowwhen every spawn is approved andPermissionResultDeny(message=reason)otherwise.Backwards compatibility
Fully opt-in. The feature is a new module that changes nothing unless you wire
can_use_toolyourself, and even then the callback only evaluates when theCLAUDE_AGENT_SDK_SHELL_PERMISSIONSenvironment variable is truthy; when unsetit defers to the provided
fallback(or allows), so existing behavior ispreserved exactly. Pass
env_var=Noneto opt out of the environment gate.bashlexis an optional dependency (pip install "claude-agent-sdk[shell-permissions]"), imported lazily.Test coverage
44 unit tests (
tests/test_shell_permissions.py; the async ones run under bothasyncio and trio). Categories:
;sequences,
&&/||, redirects, pipe-to-conditional-awk,cd-then-command.arithmetic expansion,
eval/source/./exec, control flow, malformedinput; plus a single-quote literal-backtick negative case.
cd, pipeisolation, rejected
cdforms, non-spawningexport.argv/cwd delivery, a synthetic read-only policy, empty command.
can_use_tooladapter: opt-in env gate (on/off), fallback deferral,allow/deny mapping, fail-safe deny surfaced to the caller, non-shell-tool and
missing-command deferral, custom tool/command key.
Example policies in tests and in
examples/shell_permission_evaluator.pyaresynthetic and illustrative; the engine ships no policy of its own.