Skip to content

Claude Code - Skip run_tool HITL elicitation in bypassPermissions mode#35

Merged
eshwar-sundar-glean merged 4 commits into
mainfrom
skip-elicitation-in-bypass-permissions
Jul 1, 2026
Merged

Claude Code - Skip run_tool HITL elicitation in bypassPermissions mode#35
eshwar-sundar-glean merged 4 commits into
mainfrom
skip-elicitation-in-bypass-permissions

Conversation

@eshwar-sundar-glean

@eshwar-sundar-glean eshwar-sundar-glean commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What

When Claude Code is launched with --dangerously-skip-permissions (permission_mode: "bypassPermissions"), the user has opted out of every approval prompt for the session. The plugin's own HITL elicitation popup is then a redundant second gate — and, worse, when that elicitation times out it currently blocks the run_tool call entirely.

This change makes the plugin skip its elicitation gate and execute directly when — and only when — the session is in bypassPermissions mode. Scoped deliberately to run_tool and to bypassPermissions alone.

Why it needs a hook → server handoff

The elicitation popup is fired by the MCP server (handleRunToolmcpServer.elicitInput), but the live permission mode is only visible to the PreToolUse hook (via its stdin permission_mode field). There is no env var or MCP initialize field that carries the permission mode to the server, and the mode can change mid-session — so the hook is the only channel.

How

  • plugins/glean/hooks/auto-approve-run-tool.mjs — on every glean run_tool call (while ENABLE_HITL is on), writes { permission_mode, ts } to <CLAUDE_PLUGIN_DATA|~/.glean>/glean-hitl-mode/<sessionId>.json. Best-effort; a write failure never affects the existing auto-approve decision.
  • src/tools/run-tool.ts — before eliciting, reads that per-session marker; if the mode is bypassPermissions, skips the popup and calls the tool directly.

The base dir + session-id sanitization are kept in lockstep across the hook, run-tool.ts, and start.sh's PLUGIN_DATA_DIR export.

Safety

  • Ordering: PreToolUse strictly precedes the tool call, so the marker is always fresh for the call being gated.
  • Resume: a session first launched with --dangerously-skip-permissions and later resumed without it (same session id) has its leftover bypassPermissions marker overwritten with the resumed mode by the hook on the resumed session's first run_tool call — before the server reads it — so the gate re-engages. No stale-marker leak across resume.
  • Fail-safe: a missing / malformed marker, or a session-id mismatch, resolves to "unknown" → the gate is kept (fails toward prompting, never toward a silent bypass).
  • Session-keyed: a bypass in one session can't suppress prompts in another concurrent session.

Testing

  • npm run typecheck — clean
  • npm test — 179 pass, incl. server cases (bypass skips elicit; default / absent / other-session all still elicit) and hook cases (marker written / session-keyed / sanitized; overwrites a leftover bypass marker on resume; nothing written when HITL off, non-glean, or mode absent).
  • npm run builddist/index.js rebuilt.

Verification still worth doing end-to-end

Two assumptions the implementation is safe against but that confirm the feature actually fires:

  1. PreToolUse hooks run under --dangerously-skip-permissions (strongly implied by the docs; bypassPermissions is a documented hook permission_mode value).
  2. The hook's session_id equals the CLAUDE_CODE_SESSION_ID that start.sh feeds the server as GLEAN_SESSION_ID.

Both are confirmed by running once with the flag and checking that a marker lands under ~/.glean/glean-hitl-mode/.

When Claude Code runs with --dangerously-skip-permissions, the user has opted out of every approval prompt, so the plugin's own HITL elicitation popup is a redundant gate. The PreToolUse hook now records Claude Code's live permission_mode to a per-session marker under the plugin data dir (keyed by session id, written on each run_tool call). handleRunTool reads that marker and, only when the mode is bypassPermissions, skips elicitation and executes directly.

Fail-safe: a missing/malformed marker or a session-id mismatch keeps the gate, and session-keying prevents a bypass in one session from suppressing prompts in another. Scoped to run_tool and to bypassPermissions alone.
@eshwar-sundar-glean eshwar-sundar-glean changed the title Skip run_tool HITL elicitation in bypassPermissions mode Claude Code - Skip run_tool HITL elicitation in bypassPermissions mode Jul 1, 2026
@eshwar-sundar-glean
eshwar-sundar-glean marked this pull request as ready for review July 1, 2026 10:12
Comment thread src/tools/run-tool.ts Outdated

@swarup-padhi-glean swarup-padhi-glean left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious why we store a timestamp field in the marker

Comment thread plugins/glean/hooks/auto-approve-run-tool.mjs
Address the case where a --dangerously-skip-permissions session is later resumed WITHOUT the flag (same session id): its leftover bypassPermissions marker must not keep suppressing the gate. The PreToolUse hook already rewrites the per-session marker with the current permission_mode on every run_tool call, and PreToolUse runs before the tool, so the resumed session overwrites the stale marker with its live mode before the server reads it. Added a hook test asserting the overwrite, and documented the resume-safety guarantee on the server read.
permissionModeMarkerPath()'s fallback used os.tmpdir() while the PreToolUse hook writes to ~/.glean when PLUGIN_DATA_DIR and CLAUDE_PLUGIN_DATA are both unset. start.sh always sets PLUGIN_DATA_DIR so this was masked in production, but a direct node/tsx launch (no start.sh) would read a different path than the hook wrote. Fall back to ~/.glean so the read and write paths stay in lockstep, matching the documented behavior.
Comment thread src/tools/run-tool.ts Outdated
Per review: the PreToolUse hook (write path) cannot see the server-only PLUGIN_DATA_DIR that start.sh derives, so it keys off CLAUDE_PLUGIN_DATA || ~/.glean. The server's read path led with PLUGIN_DATA_DIR, which was an asymmetry even though start.sh makes them resolve identically. Drop PLUGIN_DATA_DIR from the read path so both sides use the exact same expression. Tests stub CLAUDE_PLUGIN_DATA accordingly.
Comment thread src/tools/run-tool.ts
process.env.PLUGIN_DATA_DIR ||
process.env.CLAUDE_PLUGIN_DATA ||
path.join(os.homedir(), ".glean");
process.env.CLAUDE_PLUGIN_DATA || path.join(os.homedir(), ".glean");

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now this is clean because this is only applicable to claude and cursor is not regressed because of this

@eshwar-sundar-glean
eshwar-sundar-glean merged commit 42d64ca into main Jul 1, 2026
2 checks passed
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.

3 participants