fix(hooks): initialize flashbang when its DLL cache is unavailable

This commit is contained in:
Julian lechner committed 2026-10-08 14:38:18 +02:00
1 parent 3586245408
commit 5c66f79a76
3 files changed
+46 -16

No files matched your search

+1 -1
View File
@@ -13,7 +13,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. Registered hooks are best effort: errors do not fail a Claude session. 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. Its native DLL cache is optional: a missing `LOCALAPPDATA` environment variable or an unusable cache falls back to in-process compilation instead of suppressing the notification. 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 safety utilities check command text for selected dangerous patterns; the PowerShell variant also checks some absolute paths. Neither is a complete security boundary. The verification utilities explicitly run the repository build when invoked; they are 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 safety utilities check command text for selected dangerous patterns; the PowerShell variant also checks some absolute paths. Neither is a complete security boundary. The verification utilities explicitly run the repository build when invoked; they are not an automatic requirement after each change.
+33 -3
View File
@@ -207,15 +207,45 @@ class SessionStateHookTests(unittest.TestCase):
with self.subTest(shell=shell): with self.subTest(shell=shell):
environment = dict(os.environ) environment = dict(os.environ)
if shell == 'powershell': if shell == 'powershell':
environment['LOCALAPPDATA'] = '' environment['FLASHBANG_TEST_SCRIPT'] = str(ROOT / 'shared/hooks/flashbang.ps1')
command = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', command = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
str(ROOT / 'shared/hooks/flashbang.ps1')] "function Add-Type { throw 'GUI runtime unavailable' }; & $env:FLASHBANG_TEST_SCRIPT"]
else: else:
environment['PATH'] = '' environment['PATH'] = ''
command = [executable, str(ROOT / 'shared/hooks/flashbang.sh')] command = [executable, str(ROOT / 'shared/hooks/flashbang.sh')]
result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=20) result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=20)
self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(result.returncode, 0, result.stderr)
@unittest.skipUnless(os.name == 'nt', 'Windows GUI runtime required')
def test_flashbang_cache_is_optional(self):
executable = shutil.which('powershell') or shutil.which('pwsh')
if not executable:
self.skipTest('PowerShell unavailable')
# Exercise native initialization, stopping at the UI boundary without flashing the screen.
probe = """
function New-Object {
param([string]$TypeName)
if ($TypeName -eq 'System.Windows.Forms.Form' -and ('Flashbang.Native' -as [type])) {
[Console]::WriteLine('OVERLAY_READY')
}
throw 'Stop at UI boundary'
}
& $env:FLASHBANG_TEST_SCRIPT
"""
with tempfile.TemporaryDirectory(prefix='.flashbang-test-', dir=ROOT) as directory:
blocked = Path(directory) / 'not-a-directory'
blocked.write_text('keep')
for cache in ('', str(blocked), directory):
with self.subTest(cache=cache):
environment = dict(os.environ, LOCALAPPDATA=cache,
FLASHBANG_TEST_SCRIPT=str(ROOT / 'shared/hooks/flashbang.ps1'))
result = subprocess.run([executable, '-NoProfile', '-ExecutionPolicy', 'Bypass',
'-Command', probe], env=environment, capture_output=True,
text=True, timeout=20)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn('OVERLAY_READY', result.stdout, 'Flashbang exited before creating the overlay')
self.assertEqual(blocked.read_text(), 'keep')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
+12 -12
View File
@@ -66,27 +66,27 @@ $nativeSource = @'
if (-not ('Flashbang.Native' -as [type])) { if (-not ('Flashbang.Native' -as [type])) {
# Keyed by host runtime: an assembly emitted by Windows PowerShell (.NET # Keyed by host runtime: an assembly emitted by Windows PowerShell (.NET
# Framework) is not guaranteed to load under pwsh (.NET), and vice versa. # Framework) is not guaranteed to load under pwsh (.NET), and vice versa.
$cacheDir = Join-Path $env:LOCALAPPDATA 'claude-flashbang' try {
$runtimeTag = '{0}{1}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion.Major $cacheDir = Join-Path $env:LOCALAPPDATA 'claude-flashbang'
$cacheDll = Join-Path $cacheDir "Flashbang.Native.$runtimeTag.dll" $runtimeTag = '{0}{1}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion.Major
$loaded = $false $cacheDll = Join-Path $cacheDir "Flashbang.Native.$runtimeTag.dll"
$loaded = $false
if (Test-Path -LiteralPath $cacheDll) { if (Test-Path -LiteralPath $cacheDll) {
try { Add-Type -Path $cacheDll; $loaded = $true } catch { $loaded = $false } try { Add-Type -Path $cacheDll; $loaded = $true } catch { $loaded = $false }
} }
if (-not $loaded) { if (-not $loaded) {
try {
if (-not (Test-Path -LiteralPath $cacheDir)) { if (-not (Test-Path -LiteralPath $cacheDir)) {
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
} }
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource ` Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource `
-OutputAssembly $cacheDll -OutputAssembly $cacheDll
Add-Type -Path $cacheDll Add-Type -Path $cacheDll
} catch {
# Cache unavailable (locked file, no write access) - compile in-process.
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource
} }
} catch {
# Missing profile environment or unusable cache must not suppress the flash.
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource
} }
} }