commit 07efe78bb9ceb5741c156380dfa80d19ee74882e
parent 55e7506453879f4a66d5ec694b8d895f3d9c4b83
Author: William Casarin <jb55@jb55.com>
Date: Sun, 25 Jan 2026 20:49:16 -0800
dave: fix can_use_tool callback by using ClaudeClient
- Use ClaudeClient instead of query_stream to enable control protocol
- Add PermissionMode::Default to ensure tools trigger the callback
- Update tests to use ClaudeClient and remove outdated SDK limitation notes
- Temporarily use local path for claude-agent-sdk-rs with fixes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat:
4 files changed, 42 insertions(+), 59 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1133,8 +1133,6 @@ dependencies = [
[[package]]
name = "claude-agent-sdk-rs"
version = "0.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47e08b9f18ec1810d0355942b4a2b76b777c4d368ac0d72825e1b45f07b8fe1"
dependencies = [
"anyhow",
"async-stream",
diff --git a/crates/notedeck_dave/Cargo.toml b/crates/notedeck_dave/Cargo.toml
@@ -5,7 +5,7 @@ version.workspace = true
[dependencies]
async-openai = { version = "0.28.0", features = ["rustls-webpki-roots"] }
-claude-agent-sdk-rs = "0.6"
+claude-agent-sdk-rs = { path = "/home/jb55/dev/github/tyrchen/claude-agent-sdk-rs" }
egui = { workspace = true }
sha2 = { workspace = true }
notedeck = { workspace = true }
diff --git a/crates/notedeck_dave/src/backend/claude.rs b/crates/notedeck_dave/src/backend/claude.rs
@@ -3,8 +3,8 @@ use crate::messages::{DaveApiResponse, PendingPermission, PermissionRequest, Per
use crate::tools::Tool;
use crate::Message;
use claude_agent_sdk_rs::{
- query_stream, ClaudeAgentOptions, ContentBlock, Message as ClaudeMessage, PermissionResult,
- PermissionResultAllow, PermissionResultDeny, TextBlock,
+ ClaudeAgentOptions, ClaudeClient, ContentBlock, Message as ClaudeMessage, PermissionMode,
+ PermissionResult, PermissionResultAllow, PermissionResultDeny, TextBlock,
};
use futures::future::BoxFuture;
use futures::StreamExt;
@@ -164,18 +164,25 @@ impl AiBackend for ClaudeBackend {
});
let options = ClaudeAgentOptions::builder()
+ .permission_mode(PermissionMode::Default)
.stderr_callback(Arc::new(stderr_callback))
.can_use_tool(can_use_tool)
.build();
- let mut stream = match query_stream(prompt, Some(options)).await {
- Ok(stream) => stream,
- Err(err) => {
- tracing::error!("Claude Code error: {}", err);
- let _ = tx.send(DaveApiResponse::Failed(err.to_string()));
- return;
- }
- };
+ // Use ClaudeClient instead of query_stream to enable control protocol
+ // for can_use_tool callbacks
+ let mut client = ClaudeClient::new(options);
+ if let Err(err) = client.connect().await {
+ tracing::error!("Claude Code connection error: {}", err);
+ let _ = tx.send(DaveApiResponse::Failed(err.to_string()));
+ return;
+ }
+ if let Err(err) = client.query(&prompt).await {
+ tracing::error!("Claude Code query error: {}", err);
+ let _ = tx.send(DaveApiResponse::Failed(err.to_string()));
+ return;
+ }
+ let mut stream = client.receive_response();
while let Some(result) = stream.next().await {
match result {
diff --git a/crates/notedeck_dave/tests/claude_integration.rs b/crates/notedeck_dave/tests/claude_integration.rs
@@ -7,7 +7,7 @@
//! The CLAUDE_API_KEY environment variable is read by the CLI subprocess.
use claude_agent_sdk_rs::{
- get_claude_code_version, query_stream, ClaudeAgentOptions, ContentBlock,
+ get_claude_code_version, query_stream, ClaudeAgentOptions, ClaudeClient, ContentBlock,
Message as ClaudeMessage, PermissionMode, PermissionResult, PermissionResultAllow,
PermissionResultDeny, TextBlock, ToolPermissionContext,
};
@@ -16,18 +16,6 @@ use futures::StreamExt;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
-// NOTE: SDK v0.6.3 Limitation
-// ===========================
-// The SDK defines `can_use_tool` callback in ClaudeAgentOptions but does NOT implement
-// the control protocol to invoke it. In query_full.rs, handle_control_request only
-// handles "hook_callback" and "mcp_message" subtypes - there's no handler for
-// tool permission requests.
-//
-// For interactive permission handling, either:
-// 1. Wait for a new SDK version that implements the permission callback
-// 2. Fork the SDK and add the missing control protocol handler
-// 3. Use hooks (PreToolUse) for non-interactive permission logic
-
/// Check if Claude CLI is available
fn cli_available() -> bool {
get_claude_code_version().is_some()
@@ -201,16 +189,8 @@ fn test_prompt_formatting_is_substantial() {
}
/// Test that the can_use_tool callback is invoked when Claude tries to use a tool.
-///
-/// NOTE: As of SDK v0.6.3, the can_use_tool callback is defined in ClaudeAgentOptions
-/// but the control protocol to invoke it is NOT implemented. The SDK's handle_control_request
-/// only handles "hook_callback" and "mcp_message" subtypes - there's no handler for
-/// tool permission requests.
-///
-/// This test documents this limitation - it will FAIL until the SDK implements the
-/// permission callback control protocol.
#[tokio::test]
-#[ignore = "SDK v0.6.3 does not implement can_use_tool callback - see test documentation"]
+#[ignore = "Requires Claude Code CLI to be installed and authenticated"]
async fn test_can_use_tool_callback_invoked() {
if !cli_available() {
println!("Skipping: Claude CLI not available");
@@ -241,46 +221,43 @@ async fn test_can_use_tool_callback_invoked() {
let stderr_callback = |_msg: String| {};
let options = ClaudeAgentOptions::builder()
- .max_turns(1)
+ .tools(["Read"])
+ .permission_mode(PermissionMode::Default)
+ .max_turns(3)
.skip_version_check(true)
.stderr_callback(Arc::new(stderr_callback))
.can_use_tool(can_use_tool)
.build();
// Ask Claude to read a file - this should trigger the Read tool
- let prompt = "Read the file /etc/hostname and tell me what it contains";
+ let prompt = "Read the file /etc/hostname";
- let mut stream = match query_stream(prompt.to_string(), Some(options)).await {
- Ok(s) => s,
- Err(e) => {
- panic!("Failed to create stream: {}", e);
- }
- };
+ // Use ClaudeClient which wires up the control protocol for can_use_tool callbacks
+ let mut client = ClaudeClient::new(options);
+ client.connect().await.expect("Failed to connect");
+ client.query(prompt).await.expect("Failed to send query");
// Consume the stream
+ let mut stream = client.receive_response();
while let Some(result) = stream.next().await {
- if let Err(e) = result {
- // Some errors are expected if the tool is denied or file doesn't exist
- println!("Stream message: {:?}", e);
+ match result {
+ Ok(msg) => println!("Stream message: {:?}", msg),
+ Err(e) => println!("Stream error: {:?}", e),
}
}
let count = callback_count.load(Ordering::SeqCst);
assert!(
count > 0,
- "can_use_tool callback should have been invoked at least once, but was invoked {} times. \
- NOTE: SDK v0.6.3 doesn't implement the permission callback protocol - \
- see handle_control_request in query_full.rs which only handles 'hook_callback' and 'mcp_message'",
+ "can_use_tool callback should have been invoked at least once, but was invoked {} times",
count
);
println!("can_use_tool callback was invoked {} time(s)", count);
}
/// Test that denying a tool permission prevents the tool from executing.
-///
-/// NOTE: See test_can_use_tool_callback_invoked for SDK limitation details.
#[tokio::test]
-#[ignore = "SDK v0.6.3 does not implement can_use_tool callback"]
+#[ignore = "Requires Claude Code CLI to be installed and authenticated"]
async fn test_can_use_tool_deny_prevents_execution() {
if !cli_available() {
println!("Skipping: Claude CLI not available");
@@ -314,7 +291,9 @@ async fn test_can_use_tool_deny_prevents_execution() {
let stderr_callback = |_msg: String| {};
let options = ClaudeAgentOptions::builder()
- .max_turns(2) // Allow a retry turn for Claude to respond to denial
+ .tools(["Read"])
+ .permission_mode(PermissionMode::Default)
+ .max_turns(3)
.skip_version_check(true)
.stderr_callback(Arc::new(stderr_callback))
.can_use_tool(can_use_tool)
@@ -323,14 +302,13 @@ async fn test_can_use_tool_deny_prevents_execution() {
// Ask Claude to read a file
let prompt = "Read the file /etc/hostname";
- let mut stream = match query_stream(prompt.to_string(), Some(options)).await {
- Ok(s) => s,
- Err(e) => {
- panic!("Failed to create stream: {}", e);
- }
- };
+ // Use ClaudeClient which wires up the control protocol for can_use_tool callbacks
+ let mut client = ClaudeClient::new(options);
+ client.connect().await.expect("Failed to connect");
+ client.query(prompt).await.expect("Failed to send query");
let mut response_text = String::new();
+ let mut stream = client.receive_response();
while let Some(result) = stream.next().await {
match result {
Ok(ClaudeMessage::Assistant(msg)) => {