Skip to content

Comments

feat(claude-code): add permission prompt routing for approve mode#7420

Merged
codefromthecrypt merged 5 commits intomainfrom
adrian/claude-perms
Feb 24, 2026
Merged

feat(claude-code): add permission prompt routing for approve mode#7420
codefromthecrypt merged 5 commits intomainfrom
adrian/claude-perms

Conversation

@codefromthecrypt
Copy link
Collaborator

Summary

This adds GooseMode::Approve support to the claude-code provider. Previously only Auto and SmartApprove worked. Approve was silently unsupported.

When GOOSE_MODE=approve, the provider passes --permission-prompt-tool stdio to the CLI, which routes tool permission checks through the stream-json control protocol as can_use_tool requests. The provider surfaces these as ActionRequired events, waits for user confirmation, and writes back the allow/deny response.

This is the same mechanism the official Claude Agent SDKs use. For example, the Python SDK auto-sets permission_prompt_tool_name="stdio" when a can_use_tool callback is provided (_internal/client.py:68-69), then passes --permission-prompt-tool to the CLI (_internal/transport/subprocess_cli.py:216-218).

The Provider trait gains permission_routing() and handle_permission_confirmation() with backward-compatible defaults so existing providers are unaffected. Agent::handle_confirmation tries the provider first, then falls through to the existing confirmation_tx.

Type of Change

  • Feature
  • Tests

AI Assistance

  • This PR was created or reviewed with AI assistance

Testing

CLI

Allow:

$ GOOSE_PROVIDER=claude-code GOOSE_MODEL=sonnet GOOSE_MODE=approve \
>     target/release/goose session \
>     --with-streamable-http-extension 'https://mcp.kiwi.com'

    __( O)>  ● new session · claude-code sonnet
   \____)    20260222_11 · /Users/codefromthecrypt/oss/goose
     L L     goose is ready
  ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ 0% 0/128k
🪿 Use the kiwi search-flight tool to find fastest itinerary from BKI to SYD tomorrow.
◇  Goose would like to call the above tool, do you allow?
│  Allow
│
Here are the results for **Kota Kinabalu (BKI) → Sydney (SYD)** on **23 Feb 2026**, sorted by duration:

---

## 🏆 Fastest Itinerary

| Route               | Departure → Arrival (Local) & Duration | Cabin   | Price | Book                               |
|---------------------|----------------------------------------|---------|-------|------------------------------------|
| **BKI → SIN → SYD** | 23/02 20:00 → 24/02 12:20 (13h 20m)    | Economy | €496  | [Book](https://on.kiwi.com/MVywX6) |

--snip--

Deny:

$ GOOSE_PROVIDER=claude-code GOOSE_MODEL=sonnet GOOSE_MODE=approve     target/release/goose session     --with-streamable-http-extension 'https://mcp.kiwi.com'

    __( O)>  ● new session · claude-code sonnet
   \____)    20260222_12 · /Users/codefromthecrypt/oss/goose
     L L     goose is ready
  ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ 0% 0/128k
🪿 Use the kiwi search-flight tool to find fastest itinerary from BKI to SYD tomorrow.
◇  Goose would like to call the above tool, do you allow?
│  Deny
│
The Kiwi flight search was denied — it looks like you declined the tool call. If you'd like to try again, just let me know and I'll re-run the search for the fastest **BKI → SYD** flight on **23 Feb 2026**!
  ⏱ 9.33s
  
--snip--

Related Issues

Fixes #3671

Screenshots/Demos

Screenshot 2026-02-22 at 2 43 38 PM Screenshot 2026-02-22 at 2 43 14 PM

Copilot AI review requested due to automatic review settings February 22, 2026 20:21
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements GooseMode::Approve support for the claude-code provider, enabling user-approval workflows for tool execution. Previously, only Auto and SmartApprove modes worked, while Approve mode would return an error.

Changes:

  • Adds stdio-based permission routing using Claude CLI's --permission-prompt-tool stdio flag, following the same pattern used by official Claude Agent SDKs
  • Extends the Provider trait with optional permission_routing() and handle_permission_confirmation() methods that default to backward-compatible no-op behavior
  • Routes permission checks through the Agent's confirmation handling, trying the provider first before falling through to the legacy confirmation channel

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/goose/src/providers/claude_code.rs Implements control protocol structures and permission handling; adds Initialize request on spawn; converts spawn_process to async; cleans up pending permissions on new streams
crates/goose/src/providers/base.rs Adds PermissionRouting enum and new Provider trait methods with backward-compatible defaults
crates/goose/src/agents/agent.rs Updates handle_confirmation to try provider-specific routing first; adds supports_action_required_permissions() method; includes unit tests
crates/goose-cli/src/session/mod.rs Ensures Cancel permission sends DenyOnce confirmation to provider before breaking stream
crates/goose/tests/providers.rs Adds permission approval/denial integration tests for all providers (except codex); updates test infrastructure to support permission testing

@codefromthecrypt codefromthecrypt marked this pull request as draft February 22, 2026 20:58
@github-actions
Copy link
Contributor

github-actions bot commented Feb 22, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://block.github.io/goose/pr-preview/pr-7420/

Built to branch gh-pages at 2026-02-23 19:31 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

@codefromthecrypt codefromthecrypt marked this pull request as ready for review February 23, 2026 01:22
@codefromthecrypt codefromthecrypt requested a review from a team as a code owner February 23, 2026 01:22
Copilot AI review requested due to automatic review settings February 23, 2026 01:22
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
Signed-off-by: Adrian Cole <adrian@tetrate.io>
Signed-off-by: Adrian Cole <adrian@tetrate.io>
Signed-off-by: Adrian Cole <adrian@tetrate.io>
Signed-off-by: Adrian Cole <adrian@tetrate.io>
Copilot AI review requested due to automatic review settings February 23, 2026 22:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +857 to +860
let confirmation = rx.await.unwrap_or(PermissionConfirmation {
principal_type: PrincipalType::Tool,
permission: Permission::Cancel,
});
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

When the oneshot receiver is dropped (e.g., stream cancelled), rx.await returns Err(RecvError), which triggers unwrap_or with Permission::Cancel. However, Permission::Cancel is then handled by the catch-all _ branch that denies the request, sending the same "User denied the tool call" message as an explicit deny. This conflates stream cancellation with explicit user denial, which could be confusing in logs. Consider using a more specific deny message when the receiver is dropped to distinguish between "cancelled due to stream drop" vs "user explicitly denied".

Copilot uses AI. Check for mistakes.
) {
let provider = self.provider.lock().await.clone();
if let Some(provider) = provider.as_ref() {
if provider.permission_routing() == PermissionRouting::ActionRequired
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you talk me throught what this is doing? I think I know...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TL;DR: Confirmations from the user need to get back to whoever is waiting. For LLM providers, that's goose's tool executor. For agentic providers like claude-code, that's the provider's own stream loop. This dispatches to the right one.

Recapping LLM providers not for you, but for me:

LLM providers (Anthropic, OpenAI, etc): goose executes tools itself. When a tool needs approval, dispatch_tool_calls yields an ActionRequired message to the frontend, then blocks on an mpsc channel waiting for the user's decision. When the user allows or denies, handle_confirmation() sends the answer through that channel, which unblocks dispatch_tool_calls to proceed with or skip the tool call

Now, Agentic providers (claude-code, ACP) — the CLI process executes tools. Right now this is only used for Claude-code, but ACP will be similar:

claude-code wants to run a tool in approve mode, it writes a can_use_tool JSON request to stdout via its permission-prompt-tool. The provider's stream method yields an ActionRequired message to the frontend, then blocks on a oneshot channel in pending_confirmations. Now, it can't respond to claude-code until the user decides. When the user allows or denies, handle_confirmation() finds that oneshot channel in pending_confirmations and sends the answer, which unblocks stream, which writes the allow/deny response back to claude-code's stdin.

So, all frontends (CLI, desktop, mobile, ACP) converge on handle_confirmation(). This checks: does the provider handle its own permissions? If yes (agentic), route to the provider's oneshot channel. If not (LLM), fall through to the mpsc channel.

Copy link
Collaborator

@michaelneale michaelneale left a comment

Choose a reason for hiding this comment

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

I think makes sense when clean - wasn't totally sure about one bit buy more a Q.

And does this add any types or things we need to cater to in other front ends?

@codefromthecrypt codefromthecrypt added this pull request to the merge queue Feb 24, 2026

if permission == Permission::Cancel {
output::render_text("Tool call cancelled. Returning to chat...", Some(Color::Yellow), true);
self.agent.handle_confirmation(id.clone(), PermissionConfirmation {
Copy link
Collaborator

Choose a reason for hiding this comment

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

will this need to be reflected in desktop or mobile etc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No. This code is the CLI's permission prompt and collecting the user's answer.

Desktop/mobile already have their own ToolApprovalButtons POSTs to the confirm_tool_action server endpoint, which calls the same handle_confirmation() method this PR changed. So the routing automatically applies to desktop/mobile: nothing to do here.

Note: When #7238 lands and CLI goes through goosed, this CLI-specific code path becomes dead code; the CLI would POST to confirm_tool_action just like desktop does today.


if permission == Permission::Cancel {
output::render_text("Tool call cancelled. Returning to chat...", Some(Color::Yellow), true);
self.agent.handle_confirmation(id.clone(), PermissionConfirmation {
Copy link
Collaborator

Choose a reason for hiding this comment

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

will this need to be reflected in desktop or mobile etc?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Merged via the queue into main with commit e9cef3e Feb 24, 2026
25 checks passed
@codefromthecrypt codefromthecrypt deleted the adrian/claude-perms branch February 24, 2026 00:11
lifeizhou-ap added a commit that referenced this pull request Feb 24, 2026
* main:
  Simplified custom model flow with canonical models (#6934)
  feat: simplify the text editor to be more like pi (#7426)
  docs: add YouTube short embed to Neighborhood extension tutorial (#7456)
  fix: flake.nix build failure and deprecation warning (#7408)
  feat(claude-code): add permission prompt routing for approve mode (#7420)
  docs: generate manpages (#7443)
  Blog/goose v1 25 0 release (#7433)
  fix: detect truncated LLM responses in apps extension (#7354)
  fix: removed unnecessary version for goose acp macro dependency (#7428)
  add flag to hide select voice providers (#7406)
  New navigation settings layout options and styling (#6645)
  refactor: MCP-compliant theme tokens and CSS class rename (#7275)
  Redirect llama.cpp logs through tracing to avoid polluting CLI stdout/stderr (#7434)
  refactor: change open recipe in new window to pass recipe id (#7392)
  fix: handle truncated tool calls that break conversation alternation (#7424)
  streamline some github actions (#7430)
  Enable bedrock prompt cache (#6710)
  fix: use BEGIN IMMEDIATE to prevent SQLite deadlocks (#7429)
  Display working dir (#7419)
lifeizhou-ap added a commit that referenced this pull request Feb 24, 2026
* main: (171 commits)
  fix: TLDR CLI tab in Neighborhood MCP docs (#7461)
  fix(summon): restore skill supporting files and directory path in load output (#7457)
  Simplified custom model flow with canonical models (#6934)
  feat: simplify the text editor to be more like pi (#7426)
  docs: add YouTube short embed to Neighborhood extension tutorial (#7456)
  fix: flake.nix build failure and deprecation warning (#7408)
  feat(claude-code): add permission prompt routing for approve mode (#7420)
  docs: generate manpages (#7443)
  Blog/goose v1 25 0 release (#7433)
  fix: detect truncated LLM responses in apps extension (#7354)
  fix: removed unnecessary version for goose acp macro dependency (#7428)
  add flag to hide select voice providers (#7406)
  New navigation settings layout options and styling (#6645)
  refactor: MCP-compliant theme tokens and CSS class rename (#7275)
  Redirect llama.cpp logs through tracing to avoid polluting CLI stdout/stderr (#7434)
  refactor: change open recipe in new window to pass recipe id (#7392)
  fix: handle truncated tool calls that break conversation alternation (#7424)
  streamline some github actions (#7430)
  Enable bedrock prompt cache (#6710)
  fix: use BEGIN IMMEDIATE to prevent SQLite deadlocks (#7429)
  ...
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.

Using Claude Code Provider Cannot Ask for Permission (literally cannot do anything)

3 participants