fix(hooks): ignore optional hook failures
CI / Capabilities and shell quality (push) Successful in 11s
CI / Python tests and compilation (push) Successful in 11s
CI / PowerShell tests on Ubuntu (push) Successful in 27s
CI / CI configuration security checks (push) Successful in 4s
CI / Build and inspect generated packages (push) Successful in 6s
CI / Qwen retrieval integration (push) Successful in 14m17s
CI / Bash integration and installation guards (push) Successful in 16s
CI / Installed client configuration smoke tests (push) Successful in 13s

This commit is contained in:
Julian lechner committed 2026-09-18 16:17:21 +02:00
1 parent cacb41fe28
commit 13eefe3309
9 files changed
+60 -20

No files matched your search

+1 -1
View File
@@ -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. 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. `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.
+1 -1
View File
@@ -83,7 +83,7 @@ foreach ($path in @('shared/rules','shared/skills','shared/agents','shared/hooks
foreach ($client in @('codex','claude')) { 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' } $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" } if ($registered) { Result 'PASS' "$client Stop hook is registered" } else { Result 'WARN' "$client ships hook utilities, but no Stop hook is registered" }
} }
+30
View File
@@ -186,6 +186,36 @@ class SessionStateHookTests(unittest.TestCase):
self.assertEqual(self.run_hook(shell, executable, hook, config, workspace, ''), '') self.assertEqual(self.run_hook(shell, executable, hook, config, workspace, ''), '')
self.assertEqual(list(workspace.iterdir()), []) 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__': if __name__ == '__main__':
unittest.main() unittest.main()
+4
View File
@@ -43,6 +43,7 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
try {
# PowerShell 6+ runs on Linux and macOS, where none of this applies. # PowerShell 6+ runs on Linux and macOS, where none of this applies.
if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { exit 0 } if ($PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows) { exit 0 }
@@ -150,3 +151,6 @@ try {
} finally { } finally {
$form.Dispose() $form.Dispose()
} }
} catch {
exit 0
}
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -uo pipefail
# Bash equivalent of flashbang.ps1. The visual overlay is implemented with # Bash equivalent of flashbang.ps1. The visual overlay is implemented with
# Python's standard tkinter module because Bash itself cannot create windows. # 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 exit 0
fi fi
exec python3 - "$@" <<'PY' python3 - "$@" 2>/dev/null <<'PY' || exit 0
import argparse import argparse
import re import re
import shutil import shutil
+13 -10
View File
@@ -1,12 +1,15 @@
[CmdletBinding()] [CmdletBinding()]
param() param()
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent try {
$stateDir = Join-Path $root '.ai-session' $ErrorActionPreference = 'Stop'
if ($env:AI_CONFIG_TELEMETRY -ne '0') { $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
New-Item $stateDir -ItemType Directory -Force | Out-Null $stateDir = Join-Path $root '.ai-session'
$path = Join-Path $stateDir 'telemetry.jsonl' if ($env:AI_CONFIG_TELEMETRY -ne '0') {
('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content $path New-Item $stateDir -ItemType Directory -Force | Out-Null
if ((Get-Item $path).Length -gt 1048576) { Move-Item $path ($path + '.1') -Force } $path = Join-Path $stateDir 'telemetry.jsonl'
} ('{"event":"compact","timestamp":"' + (Get-Date).ToUniversalTime().ToString('o') + '"}') | Add-Content $path
& python (Join-Path $PSScriptRoot 'session-state.py') --hook if ((Get-Item $path).Length -gt 1048576) { Move-Item $path ($path + '.1') -Force }
exit $LASTEXITCODE }
& python (Join-Path $PSScriptRoot 'session-state.py') --hook 2>$null
} catch {}
exit 0
@@ -1,4 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param() param()
& python (Join-Path $PSScriptRoot 'session-state.py') --hook try {
exit $LASTEXITCODE $ErrorActionPreference = 'Stop'
& python (Join-Path $PSScriptRoot 'session-state.py') --hook 2>$null
} catch {}
exit 0
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -uo pipefail
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) 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"; } [ "${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 #!/usr/bin/env bash
set -euo pipefail set -uo pipefail
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd) 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