Skip to content

feat(hook): one-click clear-context + bypass via native plan-accept + keystroke injection#693

Open
AgileInnov8tor wants to merge 7 commits into
backnotprop:mainfrom
AgileInnov8tor:main
Open

feat(hook): one-click clear-context + bypass via native plan-accept + keystroke injection#693
AgileInnov8tor wants to merge 7 commits into
backnotprop:mainfrom
AgileInnov8tor:main

Conversation

@AgileInnov8tor
Copy link
Copy Markdown
Contributor

@AgileInnov8tor AgileInnov8tor commented May 11, 2026

Summary

Proper replacement for the reverted PR #668 (Expose bypass clear reminder permission mode).

PR #668 was reverted because the hook emitted a /clear reminder that was never actually displayed — a silent no-op. This PR takes a different approach: instead of trying to do the impossible (hooks cannot clear context directly), it defers to Claude Code's own native primitive.

What changed

Four commits:

  1. Wire truthful approval semantics (9096eff, c7727b9) — thread permissionMode and a new deferToNativeForClear flag through the decision pipeline so the hook knows when to defer vs. when to nudge.

  2. Consent-gated native deferral (ff79946) — when user picks "Approve + Bypass + Clear Context (native)":

    • Hook writes showClearContextOnPlanAccept: true to ~/.claude/settings.json (consent-gated, atomic write with retry)
    • Hook exits 0 with empty stdout (native passthrough)
    • Claude Code's built-in plan-accept dialog appears and shows "Yes, clear context and bypass permissions" (because the setting is now on)
  3. Keystroke injection for one-click UX (12cddf7, apps/hook/server/keystrokeInjector.ts) — before exiting 0, spawn a detached background process that fires "1\n" into the CC terminal after 600ms:

    • tmux ($TMUX_PANE): tmux send-keys -t <pane> 1 Enter (no accessibility permissions needed)
    • macOS non-tmux: osascript iterating {warp, iTerm2, Terminal} via System Events
    • Linux/Windows without tmux: no-op (user sees native dialog, presses 1 manually)
    • Silent-fail: accessibility denied or terminal not found → falls back to manual press, no crash
  4. Slop cleanup (fb6c44d) — no behavior change; removes AI-generated bloat from keystrokeInjector:

    • keystrokeInjector.ts 78→39 lines: delete 16-line module docblock + 5-line JSDoc; inline two single-use builder functions; unify the duplicated Bun.spawn+.unref() into a single call
    • keystrokeInjector.test.ts 118→107 lines: extract setPlatform(v) helper replacing 5× repeated Object.defineProperty blocks

Why not a hook clearContext field?

CC's updatedPermissions types are addRules/setMode/removeRules. There is no clearContext entry (verified by exhaustive string scan of the CC 2.1.138 binary). The plan-accept TUI dialog is hardcoded in CC's React renderer and cannot be bypassed from a hook. The native passthrough + keystroke injection is the only viable path that preserves true context-clearing without patching CC.

User experience

Before: Review plan in plannotator → click → still have to press "1" in terminal

After: Review plan in plannotator → click "Approve + Bypass + Clear Context (native)" → context cleared, bypass mode set, plan executes — zero additional keystrokes

Test plan

  • bun test packages/server packages/editor apps/hook — 265/265 pass
  • bun run build:hook — clean build
  • Manual smoke test on WarpTerminal (macOS): ExitPlanMode → plannotator UI → click entry → CC terminal auto-selects option 1 → plan executes
  • Headless probes (5 scenarios): with-consent, no-consent, already-enabled, no-settings, malformed-settings — all pass

Notes

  • Consent for the settings mutation lives at ~/.plannotator/consent/clear-context-setting.json
  • The showClearContextOnPlanAccept setting defaults to false in CC (changelog: "hidden by default in plan mode"); this PR turns it on consent-gated, not globally
  • Keystroke delay (600ms) is configurable in code; conservative enough for CC dialog render time (~200–500ms)

🤖 Generated with Claude Code

AgileInnov8tor and others added 7 commits May 5, 2026 15:05
Thread a clear-context reminder flag through approval decisions, expose a Claude Code-only approval entry that requests bypass mode, and keep the hook response honest by emitting a reminder instead of claiming context was cleared.

Constraint: Claude Code PermissionRequest hooks have no documented clearContext response field and bypassPermissions is only a request when the mode is available.

Rejected: adding a permission-mode enum or undocumented clearContext field | those would misrepresent hook capabilities and broaden the contract.

Confidence: high

Scope-risk: moderate

Directive: Do not claim Plannotator clears context until Claude Code documents a hook field for that behavior; keep reminder copy truthful.

Tested: bun test packages/server apps/hook; bun test packages/editor/wideMode.test.ts packages/ui/hooks/useAgentSettings.test.ts; bun test; bun run build:review; bun run build:hook; bun run typecheck; git diff --check --cached.

Not-tested: Browser warning-dialog replay and interactive Claude Code smoke test.

Co-authored-by: OmX <omx@oh-my-codex.dev>
Constraint: Claude Code hooks cannot clear context directly; the new action remains a truthful reminder plus bypass-mode approval.
Rejected: Sending OpenCode agent-switch state for Claude Code | it leaked build (?) UI state and misleading payload fields.
Confidence: high
Scope-risk: narrow
Directive: Keep OpenCode agent switching gated to opencode-origin approval payloads.
Tested: bun run typecheck; bun test; bun run build:review; bun run build:hook
Not-tested: Manual browser click-through of the Claude Code dropdown.
Route Claude Code plan approvals through explicit bypass payloads, consent-gated native clear-context deferral, and a fallback /clear nudge so selecting the clear-context mode no longer collapses into a silent approval no-op. The active ignored hook bundle was rebuilt locally after this source change.

Constraint: Claude Code hooks cannot directly clear context; native clear requires showClearContextOnPlanAccept and user consent.\nRejected: Treating bypassPermissionsClearReminder as a raw permissionMode | Claude Code only accepts bypassPermissions on the wire and would ignore the local UI-only value.\nConfidence: high\nScope-risk: moderate\nDirective: Rebuild apps/hook/dist/index.html after changing plan-review UI because the local plannotator launcher imports the ignored dist bundle at runtime.\nTested: git diff --check; bun run typecheck; bun test; bun run build:review; bun run build:hook; fixed-port hook smoke for /api/settings-status and native-clear /api/approve.\nNot-tested: Manual click-through in Claude Code native plan-accept dialog.
…ction

When user clicks "Approve + Bypass + Clear Context (native)" in plannotator
UI, the hook now spawns a detached background process before exiting 0
(native passthrough). The process injects "1\n" into the CC terminal after
a 600ms delay, auto-selecting "Yes, clear context and bypass permissions"
without a manual keypress.

Detection priority:
  1. tmux ($TMUX_PANE) → tmux send-keys (no accessibility permissions needed)
  2. macOS → osascript iterating {warp, iTerm2, Terminal}
  3. Linux/Windows without tmux → no-op (falls back to manual press)

spawnKeystrokeInjector() detaches via Bun.spawn + .unref() — parent exits
immediately, child fires after delay.

7 unit tests added (265/265 pass). Build clean. Smoke-tested on WarpTerminal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pawn

Structural cleanup of the keystroke injector without behavior changes:

- Delete 16-line module docblock + 5-line JSDoc (over-commented for 78 lines)
- Inline buildTmuxScript() and buildOsascriptScript() — single-use helpers
- Unify the duplicated Bun.spawn+.unref() into one call at the end: both
  branches now set `script: string | null`, then a single spawn runs if set
- `KNOWN_MACOS_TERMINALS as const` → plain string[] (not a readonly tuple)

Test cleanup:
- Extract setPlatform(v) helper — replaces 5× Object.defineProperty blocks

78→39 lines (implementation), 118→107 lines (tests). 7/7 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Warp ships as Warp.app/Contents/MacOS/stable, so its macOS process name
is "stable" — not "warp". The previous code checked
`exists (application process "warp")` via System Events, which always
returned false on Warp, leaving the keystroke never injected and the
CC plan-accept TUI unattended.

Fix: check `application "Warp" is running` (bundle-name lookup, works
regardless of the binary name) and activate Warp directly before
sending keystrokes. Fall through to process-name search for iTerm2
and Terminal unchanged.

Test: update the osascript assertion to match the new Warp check.
7/7 tests pass.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Constraint: Approved Ralph plan required saved bypassPermissionsClearReminder to nudge /clear without native fresh-thread deferral.\nRejected: Reusing native clear as the default | it can restart or open a fresh thread unexpectedly.\nConfidence: high\nScope-risk: moderate\nDirective: Treat deferToNativeForClear as an explicit native/fresh-thread escape hatch only; do not wire it to saved reminder mode.\nTested: bun test packages/editor/approvalBody.test.ts apps/hook/server/keystrokeInjector.test.ts; git diff --check scoped files; bun run typecheck; bun test; bun run build:hook; architect verification approved.\nNot-tested: Interactive manual browser smoke of Claude Code native dialog selection.
@backnotprop
Copy link
Copy Markdown
Owner

Before: Review plan in plannotator → click → still have to press "1" in terminal

Can you explain what do you mean by this? 'Cause right now it's all automated. You just approve and the agent goes on.

@AgileInnov8tor
Copy link
Copy Markdown
Contributor Author

The "before" is specific to the "Approve + Bypass + Clear Context (native)" option, not the standard approve flow which is already zero keystrokes.

Background: showClearContextOnPlanAccept: true must be set in ~/.claude/settings.json for the native dialog to appear. The hook writes this on first use (consent gated). Once set, CC waits for the user to press 1 in the terminal. This PR automates that keypress via tmux/osascript.

@backnotprop
Copy link
Copy Markdown
Owner

Okay, I think I understand. So this would require the user to turn that on and then Instead of auto approving the plan, it just pauses and then the user gets the option to clear context.

@AgileInnov8tor
Copy link
Copy Markdown
Contributor Author

AgileInnov8tor commented May 13, 2026 via email

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