6d501c2958
The hard block matched raw command text, but a shell resolves quote splicing (`kick"start"`) and backslash escaping (`kick\start`) into the literal verb before execution. So `launchctl kick"start" -k gui/501/ai.hermes.gateway` ran exactly as the blocked `kickstart` form while both the non-bypassable block and the approval detector missed it — leaving an approval-bypassing gateway self-lifecycle operation reachable. contains_gateway_lifecycle_command now runs a second pass over shlex-tokenized command segments, where quotes and escapes are already resolved. It stays anchored on a hermes-gateway identifier, so prose and non-gateway hermes services are unaffected. Because this function is the single choke point _contains_unsafe_gateway_action calls at every recursion level, referenced-script and `sh -c` payload scanning inherit the fix. tools/approval.py had the same gap for quote splices: backslash escapes are stripped by _normalize_command_for_detection, but quote splicing in an ARGUMENT position is not touched by _deobfuscate_shell_word_for_detection (scoped to command-position words, deliberately — widening it would let quoted prose match the destructive patterns). It now delegates to the fixed guard as a last check, so an ordinary pattern match still wins and keeps its more specific reason string. Tests: quoted, single-quoted and backslash-spliced verbs across the launchctl/systemctl/hermes branches, the spliced gateway identifier itself, a splice nested in an `sh -c` payload (resolves one level deeper, asserted at the recursive entry point terminal_tool actually calls), plus negative cases proving prose and non-gateway labels stay unblocked. Verified on Windows: no regressions — the 10 remaining failures across tests/tools/test_approval.py, tests/hermes_cli/test_gateway_restart_loop.py and tests/cron are identical on the unmodified baseline (POSIX file modes, symlink privileges, and /bin/bash script paths). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>