Skip to content

feat(hooks): daemon-first shim + opt-in --daemon install (B, stage 3b)#613

Merged
Necmttn merged 1 commit into
mainfrom
feat/hook-shim
Jun 25, 2026
Merged

feat(hooks): daemon-first shim + opt-in --daemon install (B, stage 3b)#613
Necmttn merged 1 commit into
mainfrom
feat/hook-shim

Conversation

@Necmttn

@Necmttn Necmttn commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Context

Stage 3b — the client half of the hybrid dispatcher and the realized latency win, on #612 (the /hooks/eval endpoint). ax hooks install --all --daemon swaps the spawned dispatcher for an effect-free shim that POSTs to the warm daemon and only pays the effect parse on fallback.

Opt-in — default --all (direct dispatcher) is unchanged, so existing installs are unaffected.

What

  • shim-core.ts (@ax/hooks-sdk/shim-core, effect-free):
    • withForwardedEnv — inject the _ax_env bypass allowlist into the event so the daemon honors the agent's ALLOW_MAIN_WRITE / ALLOW_BRANCH_CHECKOUT / ALLOW_DIRTY_MAIN_MUTATION / AX_SPEND_MODE.
    • runShim — POST daemon-first (2s timeout); on down / timeout / non-2xx, lazy-import the sibling dispatch bundle and run guards locally with the already-read stdin. The standalone bundle is 1.8 KB vs the dispatcher's ~0.9 MB — the fast path is a tiny spawn + a loopback fetch, no effect parse.
  • dispatch.ts — extract runDispatchFromStdin (the shim's fallback entry).
  • Scaffold + embed the shim (SHIM_NAME / shimScaffoldContent, sibling-ext aware: .ts falls back to dispatch.ts, embedded .js to dispatch.js).
  • Install--all --daemon installs the shim; dispatcherFamilyCommands + a keepCommand arg make a dispatch↔shim switch remove the other entry so they never double-fire.

LIVE-verified

Against a source-booted ax serve (the /hooks/eval route is DB-free, so serve boots without the DB):

scenario result
daemon up · Edit-on-main block (warm guard eval over HTTP)
daemon up · Edit-on-main + forwarded ALLOW_MAIN_WRITE=1 allow — env round-tripped through the daemon (not in process.env)
daemon up · Read allow
daemon killed · Edit-on-main still blocks via the local-bundle fallback

Plus unit tests (withForwardedEnv / isDaemonOutcome / hookEvalUrl, family-switch removal, shim scaffold). 414 hooks tests pass; typecheck (hooks-sdk + axctl) exit 0.

B complete

  1. feat(hooks): one-shot ax hooks install --all + repo-free install nudge #603 install --all + repo-free nudge
  2. feat(hooks): dispatcher core — multiplex all guards in one process (B, stage 1) #605 dispatch core · feat(hooks): scaffold + embed the single dispatcher (B, stage 2) #606 scaffold+embed · feat(hooks): flip install --all to the dispatcher + migrate legacy (B, stage 2b) #607 --all→dispatcher+migrate
  3. feat(hooks): daemon /hooks/eval fast-path + guard env-forwarding (B, stage 3a) #612 daemon /hooks/eval endpoint + env-forwarding (server half)
  4. This PR (3b) — the shim + --daemon opt-in (client half, latency win)

Default install = the dispatcher (install-size + correctness). --daemon = the opt-in latency path, transparent (same bundle offline).

🤖 Generated with Claude Code

The client half of the hybrid - and the realized latency win. `ax hooks install
--all --daemon` swaps the spawned dispatcher for an EFFECT-FREE shim that POSTs
to the warm /hooks/eval (#612) and only pays the effect parse on fallback.

- shim-core.ts (@ax/hooks-sdk/shim-core, effect-free): `withForwardedEnv`
  (inject the `_ax_env` bypass allowlist), `runShim` (POST daemon-first with a
  2s timeout; on down/timeout/non-2xx LAZY-import the sibling dispatch bundle
  and run guards locally with the already-read stdin). Standalone bundle = 1.8 KB
  vs the dispatcher's ~0.9 MB - the fast path is a tiny spawn + a loopback fetch,
  no effect parse.
- dispatch.ts: extract `runDispatchFromStdin` (the shim's fallback entry).
- Scaffold + embed the shim (`SHIM_NAME`/`shimScaffoldContent`, sibling-ext aware
  so .ts falls back to dispatch.ts and the embedded .js to dispatch.js).
- install: `--all --daemon` installs the shim; `dispatcherFamilyCommands` +
  `keepCommand` make a dispatch<->shim switch remove the other entry (no
  double-fire). Default `--all` (direct dispatcher) unchanged -> zero risk.

LIVE-verified against a source-booted `ax serve`:
- daemon up: Edit-on-main -> block (warm); + forwarded ALLOW_MAIN_WRITE=1 (in
  the payload, NOT process.env) -> allow; Read -> allow.
- daemon killed: Edit-on-main -> still blocks via the local-bundle fallback.
Plus unit tests (withForwardedEnv / isDaemonOutcome / hookEvalUrl, family-switch
removal, shim scaffold). 414 hooks tests pass; typecheck (sdk + axctl) exit 0.

Completes B. Default install is the dispatcher (size + correctness); `--daemon`
is the opt-in latency path, transparent (falls back to the same bundle offline).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying ax with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6a46573
Status: ✅  Deploy successful!
Preview URL: https://b0a6610a.ax-62d.pages.dev
Branch Preview URL: https://feat-hook-shim.ax-62d.pages.dev

View logs

@Necmttn Necmttn merged commit 3f7bb19 into main Jun 25, 2026
3 checks passed
@Necmttn Necmttn deleted the feat/hook-shim branch June 25, 2026 09:13
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.

1 participant