Summary
The SecurityValidator.hook.ts pattern matching can be bypassed by prefixing any blocked command with environment variable assignments. Commands like LANG=C rm -rf / pass through
undetected because the pattern rm -rf does not match when preceded by LANG=C .
Claude Code v2.1.38 (2026-02-10) fixed this same issue in its own built-in permission matching system. However, SecurityValidator.hook.ts performs its own independent pattern matching
against patterns.yaml, and that layer remains vulnerable.
Environment
- PAI Version: v2.5
- Claude Code: v2.1.38
- OS: macOS (Darwin 25.2.0), likely affects all platforms
Steps to Reproduce
- Add a blocked pattern in
patterns.yaml, e.g. pattern matching rm -rf
- Run:
LANG=C rm -rf /some/path
- SecurityValidator allows the command because
matchesPattern() receives the full string including the env prefix
- The pattern
rm -rf does not match LANG=C rm -rf /some/path at the expected position
Additional bypass variants:
A=1 B=2 dangerous-command (multiple env vars)
FOO="bar baz" dangerous-command (quoted values)
SPACED=val dangerous-command (leading whitespace)
Expected Behavior
LANG=C rm -rf / should be caught by any pattern that matches rm -rf, regardless of environment variable prefixes.
Actual Behavior
The handleBash() function passes the raw command string directly to validateBashCommand() without stripping env var prefixes. Since regex patterns in patterns.yaml typically match
the command name (e.g., rm -rf), the env prefix causes the match to fail.
Relevant Code
SecurityValidator.hook.ts, handleBash() function:
// Current code - vulnerable
const command = typeof input.tool_input === 'string'
? input.tool_input
: (input.tool_input?.command as string) || '';
const result = validateBashCommand(command); // raw command with env prefix
Suggested Fix
Two changes in SecurityValidator.hook.ts:
- Add normalization function (before the // Pattern Matching section):
// ========================================
// Command Normalization
// ========================================
/**
* Strip leading environment variable assignments from a command.
* Prevents bypass like: LANG=C rm -rf /
* where "rm -rf /" would not match patterns if the env prefix is present.
* Handles multiple chained assignments: A=1 B=2 actual-command args
*/
function stripEnvPrefix(command: string): string {
return command.replace(/^(\s*[A-Za-z_][A-Za-z0-9_]*=(('[^']*'|"[^"]*"|[^\s])*)\s+)+/, '');
}
- Use normalized command in handleBash():
// Strip env var prefixes before pattern matching to prevent bypass
// e.g., "LANG=C rm -rf /" should still match "rm -rf" patterns
const normalizedCommand = stripEnvPrefix(command);
const result = validateBashCommand(normalizedCommand);
The original command is preserved for logging — normalization is only applied to pattern matching.
Testing
Verified with 8 test cases:
| Input |
Normalized |
Status |
LANG=C some-cmd --flag |
some-cmd --flag |
Pass |
A=1 B=2 another-cmd |
another-cmd |
Pass |
ENV_VAR=value git status |
git status |
Pass |
echo hello (no prefix) |
echo hello |
Pass |
PATH=/usr/bin ls |
ls |
Pass |
FOO="bar baz" cmd |
cmd |
Pass |
SPACED=val cmd |
cmd |
Pass |
NO_PREFIX_HERE |
NO_PREFIX_HERE |
Pass |
Context
Claude Code v2.1.38 changelog entry:
Fixed bash permission matching for commands using environment variable wrappers
This fix was applied at the Claude Code platform level (built-in permission matching in settings.json). The SecurityValidator hook performs its own independent pattern matching and needs
the same fix applied separately.
Summary
The
SecurityValidator.hook.tspattern matching can be bypassed by prefixing any blocked command with environment variable assignments. Commands likeLANG=C rm -rf /pass throughundetected because the pattern
rm -rfdoes not match when preceded byLANG=C.Claude Code v2.1.38 (2026-02-10) fixed this same issue in its own built-in permission matching system. However,
SecurityValidator.hook.tsperforms its own independent pattern matchingagainst
patterns.yaml, and that layer remains vulnerable.Environment
Steps to Reproduce
patterns.yaml, e.g. pattern matchingrm -rfLANG=C rm -rf /some/pathmatchesPattern()receives the full string including the env prefixrm -rfdoes not matchLANG=C rm -rf /some/pathat the expected positionAdditional bypass variants:
A=1 B=2 dangerous-command(multiple env vars)FOO="bar baz" dangerous-command(quoted values)SPACED=val dangerous-command(leading whitespace)Expected Behavior
LANG=C rm -rf /should be caught by any pattern that matchesrm -rf, regardless of environment variable prefixes.Actual Behavior
The
handleBash()function passes the raw command string directly tovalidateBashCommand()without stripping env var prefixes. Since regex patterns inpatterns.yamltypically matchthe command name (e.g.,
rm -rf), the env prefix causes the match to fail.Relevant Code
SecurityValidator.hook.ts,handleBash()function:Suggested Fix
Two changes in SecurityValidator.hook.ts:
The original command is preserved for logging — normalization is only applied to pattern matching.
Testing
Verified with 8 test cases:
LANG=C some-cmd --flagsome-cmd --flagA=1 B=2 another-cmdanother-cmdENV_VAR=value git statusgit statusecho hello (no prefix)echo helloPATH=/usr/bin lslsFOO="bar baz" cmdcmdSPACED=val cmdcmdNO_PREFIX_HERENO_PREFIX_HEREContext
Claude Code v2.1.38 changelog entry:
Fixed bash permission matching for commands using environment variable wrappers
This fix was applied at the Claude Code platform level (built-in permission matching in settings.json). The SecurityValidator hook performs its own independent pattern matching and needs
the same fix applied separately.