Skip to content

feat(hook): Windows hook-decide over named pipe — enforce works on Windows (#162)#163

Merged
Ju571nK merged 4 commits into
mainfrom
feat/windows-hook-decide-162
Jun 16, 2026
Merged

feat(hook): Windows hook-decide over named pipe — enforce works on Windows (#162)#163
Ju571nK merged 4 commits into
mainfrom
feat/windows-hook-decide-162

Conversation

@Ju571nK

@Ju571nK Ju571nK commented Jun 16, 2026

Copy link
Copy Markdown
Owner

What

Implements the Windows hook-decide IPC so sigil-hook --enforce actually blocks on Windows. Previously it was a silent no-op (#162): the daemon started no decide listener on Windows and the hook client was a #[cfg(not(unix))] stub returning None, so every tool call fail-opened regardless of hook_deny_rules — false assurance for Windows users.

Changes

  • hook_event (new, cross-platform module): to_event + enum_str extracted from hook_listener so the decide path can convert events without compiling the Unix-socket listener.
  • Daemon (hook_decide_listener): per-connection logic factored into a transport-generic handle_decide_conn<S: AsyncRead + AsyncWrite>. The Unix-socket serve keeps its behavior; a new #[cfg(windows)] serve_pipe listens on a named pipe, mirroring the control plane's named-pipe server. Spawned from runtime under #[cfg(windows)] on the same shared, hot-reloadable evaluator. Windows pipes carry no peer uid → u32::MAX. hook_listener returns to #[cfg(unix)].
  • Hook (decide.rs): the #[cfg(not(unix))] request_verdict is now a real client — opens the named pipe as a blocking file (the hook is a short-lived, no-tokio process) on a worker thread bounded by the deadline via recv_timeout; any failure/timeout → None (unchanged fail-to-on_failure contract). Default pipe: \\.\pipe\sigil-hook-decide.

Verification

Cannot cross-compile Windows locally (sqlite C dep), so the Windows side was built and run on a real Windows 11 ARM64 VM:

  • cargo build + cargo clippy -D warnings for sigil-agent/sigil-hook — both exit 0.
  • Runtime, freshly-built binary, with a block-rm-rf deny pack in C:\ProgramData\Sigil\rule-packs.yaml:
    • boot log now shows hook-decide IPC listening pipe=\\.\pipe\sigil-hook-decide (absent before).
    • sigil-hook claude-code --enforce on rm -rf …permissionDecision: deny ("Blocked by Sigil rule block-rm-rf").
    • ls → allow (silent).
  • Unix unchanged: hook_event tests + hook_listener drift test + decide listener enforce_e2e green; fmt/clippy -D warnings/tests clean.

Notes / follow-ups

  • The Windows decide pipe uses the default named-pipe ACL (the Unix socket uses 0660). Verdicts are non-sensitive (allow/deny for a submitted action, no mutation, semaphore-bounded), but tightening the pipe ACL to the owning user could be a hardening follow-up.
  • This PR also carries a docs comparison image committed to the branch in a parallel session.

Closes #162

🤖 Generated with Claude Code

Ju571nK and others added 4 commits June 15, 2026 23:04
…n Windows (#162)

Windows sigil-hook --enforce was a silent no-op: the daemon started no
decide listener on Windows and the hook client was a #[cfg(not(unix))]
stub returning None, so every tool call fail-opened regardless of
hook_deny_rules — false assurance for Windows users.

Daemon: extract the per-connection decide logic into a transport-generic
handle_decide_conn (AsyncRead+AsyncWrite), keep the Unix socket serve, and
add a Windows serve_pipe over a named pipe, mirroring the control plane's
named-pipe server. Windows pipes carry no peer uid -> u32::MAX. Spawned
from runtime under #[cfg(windows)] on the same shared (hot-reloadable)
evaluator. Pipe name: \\.\pipe\sigil-hook-decide.

Hook: implement the #[cfg(not(unix))] request_verdict as a blocking
named-pipe client (the hook is a short-lived no-tokio process) on a worker
thread bounded by the deadline via recv_timeout; any failure/timeout -> None
(unchanged fail-to-on_failure contract). Default pipe path added.

Unix path unchanged (verified: decide listener + enforce_e2e green).
Windows compile + runtime verified separately on the ARM64 VM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The modules were lib-level #[cfg(unix)], so the Windows serve_pipe + its
runtime spawn failed to resolve (E0433). Make both modules cross-platform
and gate the Unix-socket-only imports/serve internally; the pure helpers
(to_event) the decide path needs are cross-platform. Caught by building the
branch on the Windows ARM64 VM (cannot cross-compile locally: sqlite C dep).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move to_event + enum_str into a transport-agnostic hook_event module so the
Windows decide path can use them without compiling the Unix-socket
hook_listener. hook_listener returns to #[cfg(unix)]; this eliminates the
dead_code/unused-import warnings that surfaced when it was forced to compile
on Windows. No behavior change (unix: hook_event tests + drift test green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Ju571nK Ju571nK merged commit 644178b into main Jun 16, 2026
5 checks passed
@Ju571nK Ju571nK deleted the feat/windows-hook-decide-162 branch June 16, 2026 07:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: sigil-hook --enforce silently no-ops (no decide listener) — false assurance; doc overstates ProgramData elevation

1 participant