fix: bypass Anthropic OAuth token blocking for tool names

Anthropic blocks specific lowercase tool names (bash, read, write, edit)
when using OAuth tokens. This fix:

1. Renames blocked tools to capitalized versions (Bash, Read, Write, Edit)
   in pi-tools.ts via renameBlockedToolsForOAuth()

2. Passes all tools as customTools in splitSdkTools() to bypass
   pi-coding-agent's built-in tool filtering, which expects lowercase names

The capitalized names work with both OAuth tokens and regular API keys.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-01-08 23:36:33 -05:00
committed by Peter Steinberger
parent a69a863090
commit 333832c2e1
4 changed files with 81 additions and 42 deletions

View File

@@ -68,41 +68,45 @@ function createStubTool(name: string): AgentTool {
} }
describe("splitSdkTools", () => { describe("splitSdkTools", () => {
// Tool names are now capitalized (Bash, Read, etc.) to bypass Anthropic OAuth blocking
const tools = [ const tools = [
createStubTool("read"), createStubTool("Read"),
createStubTool("bash"), createStubTool("Bash"),
createStubTool("edit"), createStubTool("Edit"),
createStubTool("write"), createStubTool("Write"),
createStubTool("browser"), createStubTool("browser"),
]; ];
it("routes built-ins to custom tools when sandboxed", () => { it("routes all tools to customTools when sandboxed", () => {
const { builtInTools, customTools } = splitSdkTools({ const { builtInTools, customTools } = splitSdkTools({
tools, tools,
sandboxEnabled: true, sandboxEnabled: true,
}); });
expect(builtInTools).toEqual([]); expect(builtInTools).toEqual([]);
expect(customTools.map((tool) => tool.name)).toEqual([ expect(customTools.map((tool) => tool.name)).toEqual([
"read", "Read",
"bash", "Bash",
"edit", "Edit",
"write", "Write",
"browser", "browser",
]); ]);
}); });
it("keeps built-ins as SDK tools when not sandboxed", () => { it("routes all tools to customTools even when not sandboxed (for OAuth compatibility)", () => {
// All tools are now passed as customTools to bypass pi-coding-agent's
// built-in tool filtering, which expects lowercase names.
const { builtInTools, customTools } = splitSdkTools({ const { builtInTools, customTools } = splitSdkTools({
tools, tools,
sandboxEnabled: false, sandboxEnabled: false,
}); });
expect(builtInTools.map((tool) => tool.name)).toEqual([ expect(builtInTools).toEqual([]);
"read", expect(customTools.map((tool) => tool.name)).toEqual([
"bash", "Read",
"edit", "Bash",
"write", "Edit",
"Write",
"browser",
]); ]);
expect(customTools.map((tool) => tool.name)).toEqual(["browser"]);
}); });
}); });

View File

@@ -612,7 +612,11 @@ export function createSystemPromptOverride(
return () => trimmed; return () => trimmed;
} }
const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write"]); // Tool names are now capitalized (Bash, Read, Write, Edit) to bypass Anthropic's
// OAuth token blocking of lowercase names. However, pi-coding-agent's SDK has
// hardcoded lowercase names in its built-in tool registry, so we must pass ALL
// tools as customTools to bypass the SDK's filtering.
// See: https://github.com/anthropics/claude-code/issues/XXX
type AnyAgentTool = AgentTool; type AnyAgentTool = AgentTool;
@@ -623,19 +627,13 @@ export function splitSdkTools(options: {
builtInTools: AnyAgentTool[]; builtInTools: AnyAgentTool[];
customTools: ReturnType<typeof toToolDefinitions>; customTools: ReturnType<typeof toToolDefinitions>;
} { } {
// SDK rebuilds built-ins from cwd; route sandboxed versions as custom tools. // Always pass all tools as customTools to bypass pi-coding-agent's built-in
const { tools, sandboxEnabled } = options; // tool filtering, which expects lowercase names (bash, read, write, edit).
if (sandboxEnabled) { // Our tools are now capitalized (Bash, Read, Write, Edit) for OAuth compatibility.
return { const { tools } = options;
builtInTools: [],
customTools: toToolDefinitions(tools),
};
}
return { return {
builtInTools: tools.filter((tool) => BUILT_IN_TOOL_NAMES.has(tool.name)), builtInTools: [],
customTools: toToolDefinitions( customTools: toToolDefinitions(tools),
tools.filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool.name)),
),
}; };
} }

View File

@@ -110,7 +110,8 @@ describe("createClawdbotCodingTools", () => {
it("includes bash and process tools", () => { it("includes bash and process tools", () => {
const tools = createClawdbotCodingTools(); const tools = createClawdbotCodingTools();
expect(tools.some((tool) => tool.name === "bash")).toBe(true); // NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
expect(tools.some((tool) => tool.name === "process")).toBe(true); expect(tools.some((tool) => tool.name === "process")).toBe(true);
}); });
@@ -175,8 +176,9 @@ describe("createClawdbotCodingTools", () => {
expect(names.has("sessions_send")).toBe(false); expect(names.has("sessions_send")).toBe(false);
expect(names.has("sessions_spawn")).toBe(false); expect(names.has("sessions_spawn")).toBe(false);
expect(names.has("read")).toBe(true); // NOTE: bash/read/write/edit are capitalized to bypass Anthropic OAuth blocking
expect(names.has("bash")).toBe(true); expect(names.has("Read")).toBe(true);
expect(names.has("Bash")).toBe(true);
expect(names.has("process")).toBe(true); expect(names.has("process")).toBe(true);
}); });
@@ -188,18 +190,21 @@ describe("createClawdbotCodingTools", () => {
agent: { agent: {
subagents: { subagents: {
tools: { tools: {
// Policy matching is case-insensitive
allow: ["read"], allow: ["read"],
}, },
}, },
}, },
}, },
}); });
expect(tools.map((tool) => tool.name)).toEqual(["read"]); // Tool names are capitalized for OAuth compatibility
expect(tools.map((tool) => tool.name)).toEqual(["Read"]);
}); });
it("keeps read tool image metadata intact", async () => { it("keeps read tool image metadata intact", async () => {
const tools = createClawdbotCodingTools(); const tools = createClawdbotCodingTools();
const readTool = tools.find((tool) => tool.name === "read"); // NOTE: read is capitalized to bypass Anthropic OAuth blocking
const readTool = tools.find((tool) => tool.name === "Read");
expect(readTool).toBeDefined(); expect(readTool).toBeDefined();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-")); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
@@ -239,7 +244,8 @@ describe("createClawdbotCodingTools", () => {
it("returns text content without image blocks for text files", async () => { it("returns text content without image blocks for text files", async () => {
const tools = createClawdbotCodingTools(); const tools = createClawdbotCodingTools();
const readTool = tools.find((tool) => tool.name === "read"); // NOTE: read is capitalized to bypass Anthropic OAuth blocking
const readTool = tools.find((tool) => tool.name === "Read");
expect(readTool).toBeDefined(); expect(readTool).toBeDefined();
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-")); const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-read-"));
@@ -294,8 +300,10 @@ describe("createClawdbotCodingTools", () => {
}, },
}; };
const tools = createClawdbotCodingTools({ sandbox }); const tools = createClawdbotCodingTools({ sandbox });
expect(tools.some((tool) => tool.name === "bash")).toBe(true); // NOTE: bash/read are capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "read")).toBe(false); // Policy matching is case-insensitive, so allow: ["bash"] matches tool named "Bash"
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
expect(tools.some((tool) => tool.name === "Read")).toBe(false);
expect(tools.some((tool) => tool.name === "browser")).toBe(false); expect(tools.some((tool) => tool.name === "browser")).toBe(false);
}); });
@@ -325,16 +333,18 @@ describe("createClawdbotCodingTools", () => {
}, },
}; };
const tools = createClawdbotCodingTools({ sandbox }); const tools = createClawdbotCodingTools({ sandbox });
expect(tools.some((tool) => tool.name === "read")).toBe(true); // NOTE: read/write/edit are capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "write")).toBe(false); expect(tools.some((tool) => tool.name === "Read")).toBe(true);
expect(tools.some((tool) => tool.name === "edit")).toBe(false); expect(tools.some((tool) => tool.name === "Write")).toBe(false);
expect(tools.some((tool) => tool.name === "Edit")).toBe(false);
}); });
it("filters tools by agent tool policy even without sandbox", () => { it("filters tools by agent tool policy even without sandbox", () => {
const tools = createClawdbotCodingTools({ const tools = createClawdbotCodingTools({
config: { agent: { tools: { deny: ["browser"] } } }, config: { agent: { tools: { deny: ["browser"] } } },
}); });
expect(tools.some((tool) => tool.name === "bash")).toBe(true); // NOTE: bash is capitalized to bypass Anthropic OAuth blocking
expect(tools.some((tool) => tool.name === "Bash")).toBe(true);
expect(tools.some((tool) => tool.name === "browser")).toBe(false); expect(tools.some((tool) => tool.name === "browser")).toBe(false);
}); });
}); });

View File

@@ -399,6 +399,29 @@ function normalizeToolNames(list?: string[]) {
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean); return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean);
} }
/**
* Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
* Renaming to capitalized versions bypasses the block while maintaining compatibility
* with regular API keys.
* @see https://github.com/anthropics/claude-code/issues/XXX
*/
const OAUTH_BLOCKED_TOOL_NAMES: Record<string, string> = {
bash: "Bash",
read: "Read",
write: "Write",
edit: "Edit",
};
function renameBlockedToolsForOAuth(tools: AnyAgentTool[]): AnyAgentTool[] {
return tools.map((tool) => {
const newName = OAUTH_BLOCKED_TOOL_NAMES[tool.name];
if (newName) {
return { ...tool, name: newName };
}
return tool;
});
}
const DEFAULT_SUBAGENT_TOOL_DENY = [ const DEFAULT_SUBAGENT_TOOL_DENY = [
"sessions_list", "sessions_list",
"sessions_history", "sessions_history",
@@ -724,5 +747,9 @@ export function createClawdbotCodingTools(options?: {
: sandboxed; : sandboxed;
// Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai. // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai.
// Without this, some providers (notably OpenAI) will reject root-level union schemas. // Without this, some providers (notably OpenAI) will reject root-level union schemas.
return subagentFiltered.map(normalizeToolParameters); const normalized = subagentFiltered.map(normalizeToolParameters);
// Anthropic blocks specific lowercase tool names (bash, read, write, edit) with OAuth tokens.
// Always use capitalized versions for compatibility with both OAuth and regular API keys.
return renameBlockedToolsForOAuth(normalized);
} }