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.
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.
+1 -1
View File
@@ -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" }
}
+30
View File
@@ -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()
+4
View File
@@ -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
}
+2 -2
View File
@@ -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
+13 -10
View File
@@ -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') {
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
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 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
+2 -2
View File
@@ -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