Compare commits
2
Commits
49be8edbec
...
13eefe3309
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13eefe3309 | ||
|
|
cacb41fe28 |
No files matched your search
@@ -11,11 +11,7 @@
|
||||
"Bash(Remove-Item -Recurse*)"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
"enabled": true,
|
||||
"allowUnsandboxedCommands": false,
|
||||
"failIfUnavailable": true
|
||||
},
|
||||
"sandbox": __CLAUDE_SANDBOX__,
|
||||
"env": {
|
||||
"CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "__CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY__"
|
||||
},
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ The adapters register these events:
|
||||
|
||||
The main agent alone maintains canonical task state. Hooks never write or summarize it, print its contents, or infer completed verification. They use the hook event's `cwd`, falling back to the process working directory when no workspace is supplied. Malformed event data does not fall back to another project. An absent state file is a successful no-op. Legacy state under the installed configuration root is not automatically adopted; confirm its task before migrating it. The scripts do not grant permissions, bypass approvals, or replace client permission systems.
|
||||
|
||||
When enabled during installation or update, Flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The PowerShell command omits `-WindowStyle Hidden`, which can hide or minimize the shared terminal. The PowerShell hook creates a low-opacity click-through overlay without activating another window when its native GUI APIs are available; unsupported runtimes exit safely. The Bash hook uses Python's `tkinter`, uses `xrandr` for multi-monitor bounds when available, and falls back to `notify-send` when the overlay is unavailable.
|
||||
When enabled during installation or update, Flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The PowerShell command omits `-WindowStyle Hidden`, which can hide or minimize the shared terminal. The PowerShell hook creates a low-opacity click-through overlay without activating another window when its native GUI APIs are available; unsupported runtimes exit safely. The Bash hook uses Python's `tkinter`, uses `xrandr` for multi-monitor bounds when available, and falls back to `notify-send` when the overlay is unavailable. Registered hooks are best effort: errors do not fail a Claude session.
|
||||
|
||||
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are unregistered utilities. The former checks command text for selected dangerous patterns and outside-workspace paths; it is not a complete security boundary. The latter explicitly runs the repository build when invoked; it is not an automatic requirement after each change.
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ def validate_claude(path: Path) -> None:
|
||||
if not isinstance(permissions.get(key), list) or not all(isinstance(item, str) for item in permissions[key]):
|
||||
fail(f"permissions.{key} must be an array of strings")
|
||||
sandbox = require_keys(settings.get("sandbox"), CLAUDE_SANDBOX_KEYS, "sandbox")
|
||||
if sandbox.get("enabled") is not True or sandbox.get("allowUnsandboxedCommands") is not False or sandbox.get("failIfUnavailable") is not True:
|
||||
fail("sandbox must be enabled, fail closed, and disallow unsandboxed command retries")
|
||||
if sandbox != {"enabled": False} and sandbox != {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}:
|
||||
fail("sandbox must be disabled or enabled with fail-closed settings")
|
||||
env = settings.get("env")
|
||||
if not isinstance(env, dict) or not all(isinstance(key, str) and isinstance(value, str) for key, value in env.items()):
|
||||
fail("env must be an object of string values")
|
||||
|
||||
@@ -194,9 +194,10 @@ 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}' }
|
||||
$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)
|
||||
$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)
|
||||
Set-Content -LiteralPath $path -Value $content -Encoding UTF8
|
||||
}
|
||||
|
||||
|
||||
@@ -200,6 +200,8 @@ 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}'
|
||||
content=${content//__HOOK_COMMAND__/$command}
|
||||
content=${content//__POWERSHELL_HOOK_COMMAND__/$command}
|
||||
content=${content//__HOOK_SCRIPT__/$script}
|
||||
@@ -208,6 +210,7 @@ for client in codex claude; do
|
||||
content=${content//__SESSION_POINTER_SCRIPT__/$pointer_script}
|
||||
content=${content//__STATUSLINE_COMMAND__/$command}
|
||||
content=${content//__STATUSLINE_SCRIPT__/$statusline_script}
|
||||
content=${content//__CLAUDE_SANDBOX__/$sandbox}
|
||||
printf '%s\n' "$content" > "$path"
|
||||
done
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ foreach ($path in @('shared/rules','shared/skills','shared/agents','shared/hooks
|
||||
|
||||
foreach ($client in @('codex','claude')) {
|
||||
$config = if ($client -eq 'codex') { Join-Path $root 'adapters/codex/config/config.toml' } else { Join-Path $root 'adapters/claude/config/settings.json' }
|
||||
$registered = if ($client -eq 'codex') { (Get-Content $config -Raw) -match '\[\[hooks\.Stop\]\]' } else { ((Get-Content $config -Raw | ConvertFrom-Json).hooks.PSObject.Properties.Name -contains 'Stop') }
|
||||
$registered = if ($client -eq 'codex') { (Get-Content $config -Raw) -match '\[\[hooks\.Stop\]\]' } else { (Get-Content $config -Raw) -match '"Stop"\s*:' }
|
||||
if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,36 @@ class SessionStateHookTests(unittest.TestCase):
|
||||
self.assertEqual(self.run_hook(shell, executable, hook, config, workspace, ''), '')
|
||||
self.assertEqual(list(workspace.iterdir()), [])
|
||||
|
||||
def test_hooks_ignore_missing_helper(self):
|
||||
for shell, executable in SHELLS:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=shell), tempfile.TemporaryDirectory() as directory:
|
||||
base = Path(directory)
|
||||
config = base / 'config'
|
||||
shutil.copytree(ROOT / 'shared/hooks/scripts', config / 'hooks/scripts')
|
||||
(config / 'hooks/scripts/session-state.py').unlink()
|
||||
workspace = base / 'workspace'
|
||||
workspace.mkdir()
|
||||
for hook in ('pointer', 'compact'):
|
||||
self.assertEqual(self.run_hook(shell, executable, hook, config, workspace, ''), '')
|
||||
|
||||
def test_flashbang_ignores_unavailable_runtime(self):
|
||||
for shell, executable in SHELLS:
|
||||
if not executable or not Path(executable).exists():
|
||||
continue
|
||||
with self.subTest(shell=shell):
|
||||
environment = dict(os.environ)
|
||||
if shell == 'powershell':
|
||||
environment['LOCALAPPDATA'] = ''
|
||||
command = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
||||
str(ROOT / 'shared/hooks/flashbang.ps1')]
|
||||
else:
|
||||
environment['PATH'] = ''
|
||||
command = [executable, str(ROOT / 'shared/hooks/flashbang.sh')]
|
||||
result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -48,7 +48,9 @@ foreach ($shell in @('powershell','bash')) {
|
||||
throw "Claude finish hook is incorrect in $package"
|
||||
} elseif ($content -notmatch '"defaultMode"\s*:\s*"auto"') {
|
||||
throw "Claude auto permission mode is missing in $package"
|
||||
} elseif (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true) {
|
||||
} elseif ($shell -eq 'powershell' -and ($settings.sandbox.enabled -ne $false -or $settings.sandbox.PSObject.Properties.Count -ne 1)) {
|
||||
throw "Claude sandbox must be disabled in $package"
|
||||
} elseif ($shell -eq 'bash' -and (-not $settings.sandbox.enabled -or $settings.sandbox.allowUnsandboxedCommands -ne $false -or $settings.sandbox.failIfUnavailable -ne $true)) {
|
||||
throw "Claude strict sandbox is missing in $package"
|
||||
} elseif (-not (Test-Path -LiteralPath (Join-Path $package "statusline/statusline.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })")) -or $settings.statusLine.command -notmatch "statusline\.$(if ($shell -eq 'powershell') { 'ps1' } else { 'sh' })") {
|
||||
throw "Claude status line is incorrect in $package"
|
||||
|
||||
@@ -65,7 +65,7 @@ for shell in powershell bash; do
|
||||
! grep -q 'flashbang-if-input' "$package/$file"
|
||||
! grep -q '"async"[[:space:]]*:[[:space:]]*true' "$package/$file"
|
||||
grep -q '"defaultMode"[[:space:]]*:[[:space:]]*"auto"' "$package/$file"
|
||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); assert settings["sandbox"] == {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}' "$package/$file"
|
||||
python3 -c 'import json,sys; settings=json.load(open(sys.argv[1], encoding="utf-8-sig")); expected={"enabled": False} if sys.argv[2] == "powershell" else {"enabled": True, "allowUnsandboxedCommands": False, "failIfUnavailable": True}; assert settings["sandbox"] == expected' "$package/$file" "$shell"
|
||||
statusline_extension='sh'
|
||||
[ "$shell" != powershell ] || statusline_extension=ps1
|
||||
test -f "$package/statusline/statusline.$statusline_extension"
|
||||
|
||||
@@ -43,6 +43,7 @@ param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
try {
|
||||
# PowerShell 6+ runs on Linux and macOS, where none of this applies.
|
||||
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { exit 0 }
|
||||
|
||||
@@ -150,3 +151,6 @@ try {
|
||||
} finally {
|
||||
$form.Dispose()
|
||||
}
|
||||
} catch {
|
||||
exit 0
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
|
||||
# Bash equivalent of flashbang.ps1. The visual overlay is implemented with
|
||||
# Python's standard tkinter module because Bash itself cannot create windows.
|
||||
@@ -10,7 +10,7 @@ if ! command -v python3 >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec python3 - "$@" <<'PY'
|
||||
python3 - "$@" 2>/dev/null <<'PY' || exit 0
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$stateDir = Join-Path $root '.ai-session'
|
||||
if ($env:AI_CONFIG_TELEMETRY -ne '0') {
|
||||
try {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
||||
$stateDir = Join-Path $root '.ai-session'
|
||||
if ($env:AI_CONFIG_TELEMETRY -ne '0') {
|
||||
New-Item $stateDir -ItemType Directory -Force | Out-Null
|
||||
$path = Join-Path $stateDir 'telemetry.jsonl'
|
||||
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content $path
|
||||
if ((Get-Item $path).Length -gt 1048576) { Move-Item $path ($path + '.1') -Force }
|
||||
}
|
||||
& python (Join-Path $PSScriptRoot 'session-state.py') --hook
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
& python (Join-Path $PSScriptRoot 'session-state.py') --hook 2>$null
|
||||
} catch {}
|
||||
exit 0
|
||||
@@ -1,4 +1,7 @@
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
& python (Join-Path $PSScriptRoot 'session-state.py') --hook
|
||||
exit $LASTEXITCODE
|
||||
try {
|
||||
$ErrorActionPreference = 'Stop'
|
||||
& python (Join-Path $PSScriptRoot 'session-state.py') --hook 2>$null
|
||||
} catch {}
|
||||
exit 0
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)
|
||||
[ "${AI_CONFIG_TELEMETRY:-1}" = 0 ] || { mkdir -p "$root/.ai-session" && printf '{"event":"compact","timestamp":"%s"}\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$root/.ai-session/telemetry.jsonl"; [ "$(wc -c < "$root/.ai-session/telemetry.jsonl")" -le 1048576 ] || mv -f "$root/.ai-session/telemetry.jsonl" "$root/.ai-session/telemetry.jsonl.1"; }
|
||||
python3 "$root/hooks/scripts/session-state.py" --hook
|
||||
python3 "$root/hooks/scripts/session-state.py" --hook 2>/dev/null || true
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
set -uo pipefail
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
python3 "$script_dir/session-state.py" --hook
|
||||
python3 "$script_dir/session-state.py" --hook 2>/dev/null || true
|
||||
Reference in new issue
Block a user