diff --git a/CHANGELOG.md b/CHANGELOG.md
index bb7f9e096..6eace0e33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -72,6 +72,7 @@
- Commands: return /status in directive-only multi-line messages.
- Models: fall back to configured models when the provider catalog is unavailable.
- Agent system prompt: add messaging guidance for reply routing and cross-session sends. (#526) — thanks @neist
+- Agent: bypass Anthropic OAuth tool-name blocks by capitalizing built-ins and keeping pruning tool matching case-insensitive. (#553) — thanks @andrewting19
## 2026.1.8
diff --git a/README.md b/README.md
index 52fe98fc7..700940630 100644
--- a/README.md
+++ b/README.md
@@ -457,14 +457,15 @@ Thanks to all clawtributors:
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/docs/concepts/session-pruning.md b/docs/concepts/session-pruning.md
index e5666d83f..fa3e48fb4 100644
--- a/docs/concepts/session-pruning.md
+++ b/docs/concepts/session-pruning.md
@@ -44,6 +44,7 @@ Pruning uses an estimated context window (chars ≈ tokens × 4). The window siz
## Tool selection
- `tools.allow` / `tools.deny` support `*` wildcards.
- Deny wins.
+- Matching is case-insensitive.
- Empty allow list => all tools allowed.
## Interaction with other limits
diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts
index 793f575ad..d3e1aab4f 100644
--- a/src/agents/pi-embedded-runner.ts
+++ b/src/agents/pi-embedded-runner.ts
@@ -616,7 +616,6 @@ export function createSystemPromptOverride(
// 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;
diff --git a/src/agents/pi-extensions/context-pruning.test.ts b/src/agents/pi-extensions/context-pruning.test.ts
index 3d28c519e..43c06346b 100644
--- a/src/agents/pi-extensions/context-pruning.test.ts
+++ b/src/agents/pi-extensions/context-pruning.test.ts
@@ -313,12 +313,12 @@ describe("context-pruning", () => {
makeUser("u1"),
makeToolResult({
toolCallId: "t1",
- toolName: "bash",
+ toolName: "Bash",
text: "x".repeat(20_000),
}),
makeToolResult({
toolCallId: "t2",
- toolName: "browser",
+ toolName: "Browser",
text: "y".repeat(20_000),
}),
];
diff --git a/src/agents/pi-extensions/context-pruning/tools.ts b/src/agents/pi-extensions/context-pruning/tools.ts
index 81b064767..aaebc8f4a 100644
--- a/src/agents/pi-extensions/context-pruning/tools.ts
+++ b/src/agents/pi-extensions/context-pruning/tools.ts
@@ -2,7 +2,13 @@ import type { ContextPruningToolMatch } from "./settings.js";
function normalizePatterns(patterns?: string[]): string[] {
if (!Array.isArray(patterns)) return [];
- return patterns.map((p) => String(p ?? "").trim()).filter(Boolean);
+ return patterns
+ .map((p) =>
+ String(p ?? "")
+ .trim()
+ .toLowerCase(),
+ )
+ .filter(Boolean);
}
type CompiledPattern =
@@ -39,8 +45,9 @@ export function makeToolPrunablePredicate(
const allow = compilePatterns(match.allow);
return (toolName: string) => {
- if (matchesAny(toolName, deny)) return false;
+ const normalized = toolName.trim().toLowerCase();
+ if (matchesAny(normalized, deny)) return false;
if (allow.length === 0) return true;
- return matchesAny(toolName, allow);
+ return matchesAny(normalized, allow);
};
}
diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/pi-tools-agent-config.test.ts
index db85bb798..4756e72d2 100644
--- a/src/agents/pi-tools-agent-config.test.ts
+++ b/src/agents/pi-tools-agent-config.test.ts
@@ -29,9 +29,9 @@ describe("Agent-specific tool filtering", () => {
});
const toolNames = tools.map((t) => t.name);
- expect(toolNames).toContain("read");
- expect(toolNames).toContain("write");
- expect(toolNames).not.toContain("bash");
+ expect(toolNames).toContain("Read");
+ expect(toolNames).toContain("Write");
+ expect(toolNames).not.toContain("Bash");
});
it("should apply agent-specific tool policy", () => {
@@ -63,10 +63,10 @@ describe("Agent-specific tool filtering", () => {
});
const toolNames = tools.map((t) => t.name);
- expect(toolNames).toContain("read");
- expect(toolNames).not.toContain("bash");
- expect(toolNames).not.toContain("write");
- expect(toolNames).not.toContain("edit");
+ expect(toolNames).toContain("Read");
+ expect(toolNames).not.toContain("Bash");
+ expect(toolNames).not.toContain("Write");
+ expect(toolNames).not.toContain("Edit");
});
it("should allow different tool policies for different agents", () => {
@@ -96,9 +96,9 @@ describe("Agent-specific tool filtering", () => {
agentDir: "/tmp/agent-main",
});
const mainToolNames = mainTools.map((t) => t.name);
- expect(mainToolNames).toContain("bash");
- expect(mainToolNames).toContain("write");
- expect(mainToolNames).toContain("edit");
+ expect(mainToolNames).toContain("Bash");
+ expect(mainToolNames).toContain("Write");
+ expect(mainToolNames).toContain("Edit");
// family agent: restricted
const familyTools = createClawdbotCodingTools({
@@ -108,10 +108,10 @@ describe("Agent-specific tool filtering", () => {
agentDir: "/tmp/agent-family",
});
const familyToolNames = familyTools.map((t) => t.name);
- expect(familyToolNames).toContain("read");
- expect(familyToolNames).not.toContain("bash");
- expect(familyToolNames).not.toContain("write");
- expect(familyToolNames).not.toContain("edit");
+ expect(familyToolNames).toContain("Read");
+ expect(familyToolNames).not.toContain("Bash");
+ expect(familyToolNames).not.toContain("Write");
+ expect(familyToolNames).not.toContain("Edit");
});
it("should prefer agent-specific tool policy over global", () => {
@@ -143,7 +143,7 @@ describe("Agent-specific tool filtering", () => {
const toolNames = tools.map((t) => t.name);
// Agent policy overrides global: browser is allowed again
expect(toolNames).toContain("browser");
- expect(toolNames).not.toContain("bash");
+ expect(toolNames).not.toContain("Bash");
expect(toolNames).not.toContain("process");
});
@@ -209,9 +209,9 @@ describe("Agent-specific tool filtering", () => {
// Agent policy should be applied first, then sandbox
// Agent allows only "read", sandbox allows ["read", "write", "bash"]
// Result: only "read" (most restrictive wins)
- expect(toolNames).toContain("read");
- expect(toolNames).not.toContain("bash");
- expect(toolNames).not.toContain("write");
+ expect(toolNames).toContain("Read");
+ expect(toolNames).not.toContain("Bash");
+ expect(toolNames).not.toContain("Write");
});
it("should run bash synchronously when process is denied", async () => {
@@ -229,7 +229,7 @@ describe("Agent-specific tool filtering", () => {
workspaceDir: "/tmp/test-main",
agentDir: "/tmp/agent-main",
});
- const bash = tools.find((tool) => tool.name === "bash");
+ const bash = tools.find((tool) => tool.name === "Bash");
expect(bash).toBeDefined();
const result = await bash?.execute("call1", {
diff --git a/src/agents/pi-tools.ts b/src/agents/pi-tools.ts
index abb914c4d..b42133824 100644
--- a/src/agents/pi-tools.ts
+++ b/src/agents/pi-tools.ts
@@ -403,7 +403,6 @@ function normalizeToolNames(list?: string[]) {
* 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 = {
bash: "Bash",