diff --git a/adapters/claude/config/settings.json b/adapters/claude/config/settings.json index 5c72ea9..bd239a4 100644 --- a/adapters/claude/config/settings.json +++ b/adapters/claude/config/settings.json @@ -4,12 +4,7 @@ "allow": [ "Agent" ], - "deny": [ - "Bash(git reset --hard*)", - "Bash(git clean*)", - "Bash(rm -rf*)", - "Bash(Remove-Item -Recurse*)" - ] + "deny": __CLAUDE_DENY_RULES__ }, "sandbox": __CLAUDE_SANDBOX__, "env": { diff --git a/scripts/commands/build.ps1 b/scripts/commands/build.ps1 index eedf6c5..e61e4e0 100644 --- a/scripts/commands/build.ps1 +++ b/scripts/commands/build.ps1 @@ -4,6 +4,12 @@ $ErrorActionPreference = 'Stop' $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $shared = Join-Path $root 'shared' $output = Join-Path $root 'generated' +$sandboxSupported = [Environment]::OSVersion.Platform -eq [PlatformID]::Unix +if ($sandboxSupported) { + $kernel = (& uname -r 2>$null | Out-String).Trim() + if ($kernel -match '(?i)microsoft') { $sandboxSupported = $kernel -match '(?i)microsoft-standard-WSL2' } +} +$claudeSandbox = if ($sandboxSupported) { '{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}' } else { '{"enabled": false}' } $pluginManifest = Join-Path $root 'adapters/plugins.tsv' $capabilityManifest = Join-Path $root 'adapters/claude/capabilities.tsv' @@ -173,10 +179,15 @@ foreach ($shell in @('powershell','bash')) { $script = if ($shell -eq 'powershell') { 'flashbang.ps1' } else { 'flashbang.sh' } $statusLineScript = if ($shell -eq 'powershell') { 'statusline.ps1' } else { 'statusline.sh' } $content = Get-Content -LiteralPath $path -Raw - $sandbox = if ($shell -eq 'powershell') { '{"enabled": false}' } else { '{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}' } + $denyRules = if ($shell -eq 'powershell') { + @('PowerShell(git reset --hard*)','PowerShell(git clean*)','PowerShell(Remove-Item *-Recurse*)') + } else { + @('Bash(git reset --hard*)','Bash(git clean*)','Bash(rm -rf*)') + } + $denyRulesJson = ConvertTo-Json -InputObject $denyRules -Compress $compactScript = if ($shell -eq 'powershell') { 'Record-Compact.ps1' } else { 'record-compact.sh' } $pointerScript = if ($shell -eq 'powershell') { 'Show-SessionStatePointer.ps1' } else { 'show-session-state-pointer.sh' } - $content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript).Replace('__CLAUDE_SANDBOX__', $sandbox) + $content = $content.Replace('__HOOK_COMMAND__', $command).Replace('__POWERSHELL_HOOK_COMMAND__', $command).Replace('__HOOK_SCRIPT__', $script).Replace('__POWERSHELL_HOOK_SCRIPT__', $script).Replace('__COMPACT_SCRIPT__', $compactScript).Replace('__SESSION_POINTER_SCRIPT__', $pointerScript).Replace('__STATUSLINE_COMMAND__', $command).Replace('__STATUSLINE_SCRIPT__', $statusLineScript).Replace('__CLAUDE_SANDBOX__', $claudeSandbox).Replace('__CLAUDE_DENY_RULES__', $denyRulesJson) Set-Content -LiteralPath $path -Value $content -Encoding UTF8 } @@ -192,7 +203,7 @@ $leftoverPlaceholders = @(Get-ChildItem $output -File -Recurse | ForEach-Object # expected to remain in generated output. -cmatch keeps this case-sensitive so Python # dunder names (__main__, __name__) in flashbang.sh are not mistaken for placeholders. $content = (Get-Content -LiteralPath $_.FullName -Raw) -replace '__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__', '' - if ($content -cmatch '__[A-Z0-9_]+__') { $_.FullName } + if ($content -cmatch '__[A-Z0-9_]*[A-Z0-9][A-Z0-9_]*__') { $_.FullName } }) if ($leftoverPlaceholders.Count) { throw "Unresolved template placeholders in: $($leftoverPlaceholders -join ', ')" } diff --git a/scripts/commands/build.sh b/scripts/commands/build.sh index 889972f..c7472d3 100644 --- a/scripts/commands/build.sh +++ b/scripts/commands/build.sh @@ -11,6 +11,20 @@ done root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) shared="$root/shared" output="$root/generated" +sandbox_supported=false +platform=$(uname -s 2>/dev/null || true) +case "$platform" in + Darwin) sandbox_supported=true ;; + Linux) + kernel=$(uname -r 2>/dev/null || true) + case "$kernel" in + *microsoft*) [[ "$kernel" == *microsoft-standard-WSL2* ]] && sandbox_supported=true ;; + *) sandbox_supported=true ;; + esac + ;; +esac +sandbox='{"enabled": false}' +[ "$sandbox_supported" != true ] || sandbox='{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}' plugin_manifest="$root/adapters/plugins.tsv" capability_manifest="$root/adapters/claude/capabilities.tsv" @@ -158,8 +172,11 @@ for client in codex claude; do fi path="$output/$client-$shell/$file" content=$(<"$path") - sandbox='{"enabled": true, "allowUnsandboxedCommands": false, "failIfUnavailable": true}' - [ "$shell" != powershell ] || sandbox='{"enabled": false}' + if [ "$shell" = powershell ]; then + deny_rules='["PowerShell(git reset --hard*)","PowerShell(git clean*)","PowerShell(Remove-Item *-Recurse*)"]' + else + deny_rules='["Bash(git reset --hard*)","Bash(git clean*)","Bash(rm -rf*)"]' + fi content=${content//__HOOK_COMMAND__/$command} content=${content//__POWERSHELL_HOOK_COMMAND__/$command} content=${content//__HOOK_SCRIPT__/$script} @@ -169,6 +186,7 @@ for client in codex claude; do content=${content//__STATUSLINE_COMMAND__/$command} content=${content//__STATUSLINE_SCRIPT__/$statusline_script} content=${content//__CLAUDE_SANDBOX__/$sandbox} + content=${content//__CLAUDE_DENY_RULES__/$deny_rules} printf '%s\n' "$content" > "$path" done @@ -191,9 +209,9 @@ done # __AI_CONFIG_ROOT__ is resolved at install time, once the destination is known; it is # expected to remain in generated output, so it is excluded from the leftover check. A # single tree-wide grep (rather than one process per file) keeps this fast. -leftover_tokens=$(grep -RohE '__[A-Z0-9_]+__' "$output" 2>/dev/null | sort -u | grep -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__)$' || true) +leftover_tokens=$(grep -RohE '__[A-Z0-9_]*[A-Z0-9][A-Z0-9_]*__' "$output" 2>/dev/null | sort -u | grep -Ev '^(__AI_CONFIG_ROOT__|__POWERSHELL_COMMAND__|__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__)$' || true) if [ -n "$leftover_tokens" ]; then - offending_files=$(grep -RlE '__[A-Z0-9_]+__' "$output" 2>/dev/null | tr '\n' ' ') + offending_files=$(grep -RlE '__[A-Z0-9_]*[A-Z0-9][A-Z0-9_]*__' "$output" 2>/dev/null | tr '\n' ' ') printf 'Unresolved template placeholders (%s) in: %s\n' "$(printf '%s' "$leftover_tokens" | tr '\n' ' ')" "$offending_files" >&2 exit 1 fi