diff --git a/internal/providers/claude_cli_session.go b/internal/providers/claude_cli_session.go index 8759d82e..7b6f7e2c 100644 --- a/internal/providers/claude_cli_session.go +++ b/internal/providers/claude_cli_session.go @@ -31,8 +31,14 @@ var cliNativeToolGoclawEquivalent = map[string]string{ // cliNativeToolsAlwaysBlocked lists Claude CLI native tools with no GoClaw // policy equivalent. They are always disallowed so agent tool policy cannot // be bypassed through them. +// +// TodoRead and NotebookRead were removed here: current Claude CLI releases +// (>= 2.x) no longer register them, and a deny rule naming an unknown tool +// makes the CLI print "Permission deny rule ... matches no known tool" on +// every invocation. Their write-side counterparts TodoWrite/NotebookEdit +// still exist in the CLI and remain blocked. var cliNativeToolsAlwaysBlocked = []string{ - "Glob", "Grep", "TodoRead", "TodoWrite", "NotebookRead", "NotebookEdit", + "Glob", "Grep", "TodoWrite", "NotebookEdit", } // disallowedCLITools computes the --disallowedTools value for the Claude CLI diff --git a/internal/providers/claude_cli_session_test.go b/internal/providers/claude_cli_session_test.go index e4af199b..2c5361a9 100644 --- a/internal/providers/claude_cli_session_test.go +++ b/internal/providers/claude_cli_session_test.go @@ -123,3 +123,29 @@ func TestBuildStreamJSONInput_NoText(t *testing.T) { t.Errorf("content blocks = %d, want 1 (image only)", len(msg.Message.Content)) } } + +// TestDisallowedCLITools_NoStaleToolNames guards against deny rules that name +// tools the current Claude CLI no longer registers. A stale name makes the CLI +// print `Permission deny rule "" matches no known tool` on every +// invocation (including background episodic summarization), which buries real +// errors. TodoRead/NotebookRead were removed from the CLI in 2.x; their +// write-side counterparts must stay blocked. +func TestDisallowedCLITools_NoStaleToolNames(t *testing.T) { + blocked := disallowedCLITools(nil) // nil = fail closed: everything blocked + + got := make(map[string]bool, len(blocked)) + for _, name := range blocked { + got[name] = true + } + + for _, stale := range []string{"TodoRead", "NotebookRead"} { + if got[stale] { + t.Errorf("disallowedCLITools includes %q, which current Claude CLI no longer registers (causes 'matches no known tool' warnings)", stale) + } + } + for _, required := range []string{"TodoWrite", "NotebookEdit", "Glob", "Grep"} { + if !got[required] { + t.Errorf("disallowedCLITools missing %q — native tool without GoClaw equivalent must stay blocked", required) + } + } +}