-
Notifications
You must be signed in to change notification settings - Fork 1
[Plugins] Auto-approve run_tool's native prompt under HITL (Claude Code PreToolUse hook) #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f78a935
Prototype: flag-gated PreToolUse hook to auto-approve run_tool under …
swarup-padhi-glean 86892d3
Generalize HITL auto-approve hook to glean read-only tools; address r…
swarup-padhi-glean f52e614
Scope auto-approve hook to run_tool only (ship consensus); bump to 0.…
swarup-padhi-glean 8a47799
Merge remote-tracking branch 'origin/main' into auto-approve-run-tool…
swarup-padhi-glean File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| #!/usr/bin/env node | ||
| // PreToolUse hook for Claude Code. | ||
| // | ||
| // When HITL is enabled, run_tool is gated by the plugin's own elicitation | ||
| // prompt, so Claude Code's separate native "allow this tool?" prompt is | ||
| // redundant (the double-prompt). With ENABLE_HITL=true we auto-approve the | ||
| // run_tool call, leaving the HITL elicitation as the single gate. | ||
| // | ||
| // Safety: run_tool is read-only ONLY while HITL gates it. This hook runs only | ||
| // under Claude Code, which always advertises the elicitation capability, so | ||
| // ENABLE_HITL=true means run_tool's HITL prompt is active — never an ungated | ||
| // write. When ENABLE_HITL is not "true" the hook does nothing and the normal | ||
| // permission flow runs. | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
|
|
||
| function readStdin() { | ||
| try { | ||
| return fs.readFileSync(0, "utf-8"); | ||
| } catch { | ||
| return ""; | ||
| } | ||
| } | ||
|
|
||
| let input = {}; | ||
| try { | ||
| input = JSON.parse(readStdin()); | ||
| } catch { | ||
| // Malformed/empty input: do nothing, let the normal permission flow run. | ||
| } | ||
|
|
||
| const toolName = String(input.tool_name ?? ""); | ||
| const bareName = toolName.split("__").pop() ?? ""; | ||
| // Scope strictly to this plugin's run_tool — the tool name carries the glean | ||
| // plugin/server prefix (e.g. mcp__plugin_glean-vnext_glean__run_tool). | ||
| if (!toolName.includes("glean") || bareName !== "run_tool") { | ||
| process.exit(0); | ||
| } | ||
|
|
||
| // The hook process does not inherit the MCP server's env, so read the flag | ||
| // from the plugin's own .mcp.json. | ||
| let env = {}; | ||
| try { | ||
| const root = process.env.CLAUDE_PLUGIN_ROOT ?? "."; | ||
| const cfg = JSON.parse(fs.readFileSync(path.join(root, ".mcp.json"), "utf-8")); | ||
| env = cfg?.mcpServers?.glean?.env ?? {}; | ||
| } catch { | ||
| // No readable config: do nothing. | ||
| } | ||
|
|
||
| if (env.ENABLE_HITL === "true") { | ||
| process.stdout.write( | ||
| JSON.stringify({ | ||
| hookSpecificOutput: { | ||
| hookEventName: "PreToolUse", | ||
| permissionDecision: "allow", | ||
| permissionDecisionReason: | ||
| "Glean run_tool is gated by its own HITL elicitation prompt; suppressing the redundant native prompt while ENABLE_HITL is on.", | ||
| }, | ||
| }), | ||
| ); | ||
| } | ||
| process.exit(0); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "mcp__.*glean.*run_tool", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/auto-approve-run-tool.mjs\"" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { spawn } from "node:child_process"; | ||
| import fs from "node:fs/promises"; | ||
| import path from "node:path"; | ||
| import os from "node:os"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| const here = path.dirname(fileURLToPath(import.meta.url)); | ||
| const HOOK = path.resolve( | ||
| here, | ||
| "../plugins/glean/hooks/auto-approve-run-tool.mjs", | ||
| ); | ||
|
|
||
| async function runHook( | ||
| toolName: string, | ||
| env: Record<string, string>, | ||
| ): Promise<string> { | ||
| const root = await fs.mkdtemp(path.join(os.tmpdir(), "approve-hook-")); | ||
| await fs.writeFile( | ||
| path.join(root, ".mcp.json"), | ||
| JSON.stringify({ mcpServers: { glean: { env } } }), | ||
| ); | ||
| try { | ||
| return await new Promise<string>((resolve, reject) => { | ||
| const child = spawn("node", [HOOK], { | ||
| env: { ...process.env, CLAUDE_PLUGIN_ROOT: root }, | ||
| }); | ||
| let out = ""; | ||
| child.stdout.on("data", (d) => (out += d.toString())); | ||
| child.on("error", reject); | ||
| child.on("close", () => resolve(out)); | ||
| child.stdin.write(JSON.stringify({ tool_name: toolName })); | ||
| child.stdin.end(); | ||
| }); | ||
| } finally { | ||
| await fs.rm(root, { recursive: true, force: true }); | ||
| } | ||
| } | ||
|
|
||
| const glean = (tool: string) => `mcp__plugin_glean-vnext_glean__${tool}`; | ||
| const hitlOn = { ENABLE_HITL: "true" }; | ||
| const hitlOff = { ENABLE_HITL: "false" }; | ||
|
|
||
| describe("auto-approve-run-tool hook (Claude Code PreToolUse)", () => { | ||
| it("allows glean run_tool when HITL is on", async () => { | ||
| const out = await runHook(glean("run_tool"), hitlOn); | ||
| expect(JSON.parse(out).hookSpecificOutput.permissionDecision).toBe("allow"); | ||
| }); | ||
|
|
||
| it("never allows when HITL is off (safety)", async () => { | ||
| const out = await runHook(glean("run_tool"), hitlOff); | ||
| expect(out.trim()).toBe(""); | ||
| }); | ||
|
|
||
| it("ignores a non-glean run_tool (scoped to this plugin)", async () => { | ||
| const out = await runHook("mcp__other-server__run_tool", hitlOn); | ||
| expect(out.trim()).toBe(""); | ||
| }); | ||
|
|
||
| it("ignores glean tools other than run_tool (e.g. find_skills)", async () => { | ||
| const out = await runHook(glean("find_skills"), hitlOn); | ||
| expect(out.trim()).toBe(""); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check what does it mean to support it in Codex and Cursor as well?
Cursor - https://cursor.com/docs/hooks#pretooluse
Codex - https://developers.openai.com/codex/hooks#pretooluse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @eshwar-sundar-glean we don't have this problem in cursor/codex. We don't need to solve there.