Skip to content

fix(codex): hide passive hook prompts while preserving processing - #36

Merged
darrenzhu merged 3 commits into
mainfrom
codex/passive-hook-context
Sep 12, 2026
Merged

darrenzhu merged 3 commits into
mainfrom
codex/passive-hook-context

Conversation

@darrenzhu

@darrenzhu darrenzhu commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Codex renders a blocking Stop hook reason as an extra user message. Forge's passive observation offers and time checkpoints now queue silently at Stop and arrive as developer context on the next real prompt, preserving the user's substantive answer. Required workflow skill continuation retains its one-time blocking reminder.

Codex plugin 1.20.3. Claude Code, Cursor, and shared server behavior are unchanged.

The passive path supports deferred offers with a cooldown, snoozed wake checks, and combined work-item routing/context delivery. State writes are atomic with bounded Windows retries, and display deduplication uses a separate sidecar.

Each queued checkpoint carries a UUID in state_updates.codex_checkpoint_id. The guard validates the current UUID and frozen payload, then exclusively creates an attempt file before forwarding. Older IDs and duplicate/concurrent/ambiguous attempts are rejected, even after another checkpoint is delivered. The tracker acknowledges only the matching attempted ID. Both hooks normalize object or JSON-string state_updates before inspection/enrichment; malformed input is never spread into character fields.

Validation:

  • All 48 hook tests passed locally on Windows, including six concurrent submissions allowing exactly one attempt and string payloads through both hooks. JavaScript syntax and diff whitespace checks passed.
  • The original 1.20.1 desktop simulation confirmed hidden context delivery, model acknowledgment, and no synthetic hook-message bubble. The latest 1.20.2 desktop simulation was stopped before completion; the earlier live result is not an exact-revision pass for these changes.
  • Tests and adaptation notes are maintained in the internal forge-mcp repository, not this marketplace repository.

Limits: delivery needs a subsequent real prompt. Failed or interrupted checkpoint attempts may lose telemetry because they are not retried. Undelivered legacy queues receive their UUID on delivery; previously delivered id-less payloads are rejected. Host indicators and expandable tool activity can remain visible. Live remote checkpoint recording was not exercised.

Companion regression tests and adaptation notes: https://github.com/ShipToday/forge-mcp/pull/1014.

All three guard denial sites now use the supported PreToolUse permissionDecision/permissionDecisionReason envelope. Regression assertions validate that host contract. Codex CLI 0.149.1 recognized the updated denial as a PreToolUse block; the separate allow controls were rejected by execution policy, so a full positive-control integration pass is not claimed.

darrenzhu added a commit that referenced this pull request Sep 11, 2026
Review follow-up on #36. Nothing here changes the design - Stop still only
queues, delivery still rides the next real prompt - but the delivery model
had gaps and one state-write race.

session-state.cjs
- Retry the rename over the state file on EPERM/EACCES/EBUSY with a short
  bounded backoff. On Windows the rename fails whenever another hook has
  the file open, and the two PostToolUse hooks run concurrently on every
  tool call; a dropped write silently lost workflow-completion resets.
- Owner-private state dir (0700) and temp files (0600) where honoured;
  clean up leftover .tmp and .display files.

must-display.cjs
- Keep the dedup fingerprint in a .display sidecar instead of the state
  file, so there is one writer per file.

prompt-router.cjs / passive-observation.cjs
- Emit key routing and passive context together in one additionalContext
  payload, so a prompt mentioning a work item (or SHA-256, CVE-...) can no
  longer hold back a queued checkpoint whose time is already reserved.
- Linked sessions get no key advisory (canonical "silent when linked")
  but still receive passive context.
- `defer` now re-arms the fire-once latch; stop-observer re-queues the
  offer after an eight-turn cooldown instead of never.
- Per-prompt wake check for snoozed sessions (canonical behaviour), with
  the wake condition sanitised to one bounded line.
- Validate queued ids as UUIDs and ship the acknowledge command ready-made
  in the delivered context, so the model never assembles a shell command
  from state-file values.
- Hold delivery for exactly one prompt after a Forge clarification, keyed
  on a turn stamp rather than observer_blocked (which prompt-router clears
  before delivery runs).

stop-observer.cjs
- Restore the canonical skill-continuation text (SHI-787).
- Batch the per-Stop writes (fewer renames, smaller race window).
- Restore the load-bearing rationale comments.

workflow-tracker.cjs / workflow-guard.cjs
- Parse string-typed state_updates so a checkpoint never falls through to
  the completion reset.
- Move last_checkpoint_turn with last_checkpoint_at so a freshly logged
  session does not queue a near-zero checkpoint on its next Stop.
- Deny a replay of an already-processed checkpoint (the server adds
  duration deltas without dedup).
- Refresh a queued checkpoint's token snapshot at call time when the
  Codex build can rewrite tool input.
- Windows version probe: one literal command, avoiding Node 24's DEP0190
  stderr warning on every tool call.

Docs / tests / CI
- SYNC.md: what "preserve" means, the version bump rule (including the
  canonical catch-up case), the missing adaptations, expected canonical
  test failures, and the telemetry-loss sentence corrected.
- forge-autopilot SKILL.md: receipt-command contract, defer semantics,
  wake check, and the stale "directive" reference.
- 9 new hook tests (31 total); GitHub Actions on Linux + Windows, Node
  18/20/22.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… messages

Codex shows a blocking Stop hook reason as an extra user message. Forge's
passive session prompts (the tracking offer and periodic time checkpoints)
no longer block at Stop: they are saved locally and delivered once, as
developer context, with the next real prompt, so the user's answer stays
the last thing on screen. A workflow step that stalls after a local skill
still gets its one-time continuation reminder.

- The tracking offer can be deferred; it comes back after a short cooldown.
- Snoozed sessions are checked against their wake condition on each prompt.
- Work-item routing hints and passive context are delivered together.
- A delivered checkpoint is recorded once and cannot be resubmitted.
- Local session-state writes are atomic, owner-private, and more robust on
  Windows.

Codex plugin 1.20.1. Claude Code and Cursor are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@darrenzhu
darrenzhu force-pushed the codex/passive-hook-context branch from b13ef1a to d71e63d Compare September 11, 2026 06:56
@darrenzhu
darrenzhu merged commit 80d3d16 into main Sep 12, 2026
@darrenzhu
darrenzhu deleted the codex/passive-hook-context branch September 12, 2026 04:06
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.

2 participants