Explored Clawdbot project and code context.
UnderstandAnalyzed purpose of Anthropic magic string.
UnderstandExplained Clawdbot's processing of the magic string.
Understandclone https://github.com/clawdbot/clawdbot/blob/1ef2de1276eb7a123c6efd8d60b1ec52f4354b8b/src/agents/pi-embedded-runner/run.ts#L55 and check out what this thing is about, can just pull in /tmp/clawdbot/ and investigate
The user wants me to clone a GitHub repository and investigate a specific file. Let me do that.
>cd /tmp && git clone https://github.com/clawdbot/clawdbot.git 2>&1fatal: destination path 'clawdbot' already exists and is not an empty directory.
Command exited with code 128The directory already exists. Let me just pull the latest and read the file.
>cd /tmp/clawdbot && git fetch && git checkout 1ef2de1276eb7a123c6efd8d60b1ec52f4354b8b 2>&1From https://github.com/clawdbot/clawdbot
fb164b321..b709898fb main -> origin/main
* [new branch] feat/heartbeat-session-target -> origin/feat/heartbeat-session-target
* [new branch] feat/lobster-plugin -> origin/feat/lobster-plugin
* [new branch] feat/models-command -> origin/feat/models-command
* [new branch] feature/1378-zalouser-extension -> origin/feature/1378-zalouser-extension
* [new branch] fix/avatar-relative-url-validation -> origin/fix/avatar-relative-url-validation
* [new branch] fix/bluebubbles-gc-guid-resolution -> origin/fix/bluebubbles-gc-guid-resolution
* [new branch] fix/chrome-restore-prompt -> origin/fix/chrome-restore-prompt
* [new branch] fix/discord-exec-resolvedpath-validation -> origin/fix/discord-exec-resolvedpath-validation
* [new branch] fix/exec-allowlist-agentid-derivation -> origin/fix/exec-allowlist-agentid-derivation
* [new branch] fix/export-section-meta -> origin/fix/export-section-meta
* [new branch] fix/mac-node-approvals -> origin/fix/mac-node-approvals
* [new branch] fix/model-switch-persist-1435 -> origin/fix/model-switch-persist-1435
* [new branch] fix/node-daemon-run -> origin/fix/node-daemon-run
* [new branch] fix/session-memory-suppress-confirmation -> origin/fix/session-memory-suppress-confirmation
* [new branch] fix/subagent-skills-inheritance -> origin/fix/subagent-skills-inheritance
* [new branch] reminders -> origin/reminders
* [new tag] v2026.1.21 -> v2026.1.21
Note: switching to '1ef2de1276eb7a123c6efd8d60b1ec52f4354b8b'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 1ef2de127 fix: cover missing session key model switch persist (#1465) (thanks @robbyczgw-cla)Good, now let me read the file the user is interested in.
This is interesting! This is a file from the "clawdbot" project which appears to be a multi-platform chatbot framework that can run Claude (and other LLMs) in an embedded fashion. Let me look at more of the project to understand what it's all about.
Let me explore more of the project structure to get a fuller picture:
>cd /tmp/clawdbot && find . -name "*.md" -not -path "./node_modules/*" | head -20./CHANGELOG.md
./Swabble/CHANGELOG.md
./Swabble/docs/spec.md
./Swabble/README.md
./docs.acp.md
./docs/date-time.md
./docs/plugin.md
./docs/tui.md
./docs/environment.md
./docs/platforms/android.md
./docs/platforms/windows.md
./docs/platforms/linux.md
./docs/platforms/mac/menu-bar.md
./docs/platforms/mac/permissions.md
./docs/platforms/mac/bundled-gateway.md
./docs/platforms/mac/canvas.md
./docs/platforms/mac/webchat.md
./docs/platforms/mac/voice-overlay.md
./docs/platforms/mac/xpc.md
./docs/platforms/mac/voicewake.md>cd /tmp/clawdbot && ls -latotal 4160
drwxr-xr-x@ 53 badlogic wheel 1696 Jan 22 23:04 .
drwxrwxrwt 67 root wheel 2144 Jan 22 19:28 ..
drwxr-xr-x@ 4 badlogic wheel 128 Jan 21 13:42 .agent
-rw-r--r--@ 1 badlogic wheel 1097 Jan 21 13:42 .detect-secrets.cfg
-rw-r--r--@ 1 badlogic wheel 441 Jan 21 13:42 .dockerignore
-rw-r--r--@ 1 badlogic wheel 257 Jan 21 13:42 .env.example
drwxr-xr-x@ 13 badlogic wheel 416 Jan 22 23:04 .git
-rw-r--r--@ 1 badlogic wheel 19 Jan 21 13:42 .gitattributes
drwxr-xr-x@ 4 badlogic wheel 128 Jan 21 13:42 .github
-rw-r--r--@ 1 badlogic wheel 1192 Jan 21 13:42 .gitignore
-rw-r--r--@ 1 badlogic wheel 139 Jan 21 13:42 .npmrc
-rw-r--r--@ 1 badlogic wheel 107 Jan 21 13:42 .oxfmtrc.jsonc
-rw-r--r--@ 1 badlogic wheel 241 Jan 21 13:42 .oxlintrc.json
-rw-r--r--@ 1 badlogic wheel 36 Jan 21 13:42 .prettierignore
-rw-r--r--@ 1 badlogic wheel 14610 Jan 21 13:42 .secrets.baseline
-rw-r--r--@ 1 badlogic wheel 1091 Jan 21 13:42 .swiftformat
-rw-r--r--@ 1 badlogic wheel 2838 Jan 21 13:42 .swiftlint.yml
-rw-r--r--@ 1 badlogic wheel 15807 Jan 22 23:04 AGENTS.md
-rw-r--r--@ 1 badlogic wheel 29461 Jan 22 23:04 appcast.xml
drwxr-xr-x@ 6 badlogic wheel 192 Jan 21 13:42 apps
drwxr-xr-x@ 6 badlogic wheel 192 Jan 21 13:42 assets
-rw-r--r--@ 1 badlogic wheel 96288 Jan 22 23:04 CHANGELOG.md
lrwxr-xr-x@ 1 badlogic wheel 9 Jan 21 13:42 CLAUDE.md -> AGENTS.md
-rw-r--r--@ 1 badlogic wheel 1596 Jan 21 13:42 CONTRIBUTING.md
drwxr-xr-x@ 3 badlogic wheel 96 Jan 22 23:04 dist
-rw-r--r--@ 1 badlogic wheel 1347 Jan 21 13:42 docker-compose.yml
-rwxr-xr-x@ 1 badlogic wheel 5896 Jan 21 13:42 docker-setup.sh
-rw-r--r--@ 1 badlogic wheel 909 Jan 21 13:42 Dockerfile
-rw-r--r--@ 1 badlogic wheel 285 Jan 21 13:42 Dockerfile.sandbox
-rw-r--r--@ 1 badlogic wheel 570 Jan 21 13:42 Dockerfile.sandbox-browser
drwxr-xr-x@ 46 badlogic wheel 1472 Jan 22 23:04 docs
-rw-r--r--@ 1 badlogic wheel 5198 Jan 21 13:42 docs.acp.md
drwxr-xr-x@ 24 badlogic wheel 768 Jan 22 23:04 extensions
drwxr-xr-x@ 3 badlogic wheel 96 Jan 21 13:42 git-hooks
-rw-r--r--@ 1 badlogic wheel 1074 Jan 21 13:42 LICENSE
-rw-r--r--@ 1 badlogic wheel 10224 Jan 22 23:04 package.json
drwxr-xr-x@ 3 badlogic wheel 96 Jan 22 23:04 patches
-rw-r--r--@ 1 badlogic wheel 353617 Jan 22 23:04 pnpm-lock.yaml
-rw-r--r--@ 1 badlogic wheel 218 Jan 21 13:42 pnpm-workspace.yaml
-rw-r--r--@ 1 badlogic wheel 1413716 Jan 21 13:42 README-header.png
-rw-r--r--@ 1 badlogic wheel 67441 Jan 22 23:04 README.md
drwxr-xr-x@ 63 badlogic wheel 2016 Jan 22 23:04 scripts
-rw-r--r--@ 1 badlogic wheel 416 Jan 21 13:42 SECURITY.md
drwxr-xr-x@ 54 badlogic wheel 1728 Jan 21 13:42 skills
drwxr-xr-x@ 65 badlogic wheel 2080 Jan 22 23:04 src
drwxr-xr-x@ 15 badlogic wheel 480 Jan 21 13:42 Swabble
drwxr-xr-x@ 10 badlogic wheel 320 Jan 21 13:42 test
-rw-r--r--@ 1 badlogic wheel 538 Jan 21 13:42 tsconfig.json
drwxr-xr-x@ 9 badlogic wheel 288 Jan 21 13:42 ui
drwxr-xr-x@ 3 badlogic wheel 96 Jan 21 13:42 vendor
-rw-r--r--@ 1 badlogic wheel 3354 Jan 22 23:04 vitest.config.ts
-rw-r--r--@ 1 badlogic wheel 590 Jan 21 13:42 vitest.e2e.config.ts
-rw-r--r--@ 1 badlogic wheel 353 Jan 21 13:42 vitest.live.config.tsThis looks like a very comprehensive project. Let me read the README to understand what it's about.
This is Peter Steinberger's "Clawdbot" project! It's a personal AI assistant that can run on multiple messaging channels. The specific file the user linked (run.ts) is part of the "pi-embedded-runner" which runs pi (our coding agent from pi-mono) in an embedded mode within Clawdbot.
Let me look at the specific line 55 area that the user highlighted:
Now let me look at the specific area around line 55 that you linked to:
Ah, line 55 is about the "Anthropic Magic String Trigger Refusal" scrubbing. That's a safety mechanism to prevent prompt injection attacks that might try to trigger Anthropic's refusal test behavior.
Let me summarize what this Clawdbot project is about.
Summary of Clawdbot:
Clawdbot is a personal AI assistant framework by Peter Steinberger that acts as a multi-channel gateway. The key concepts:
Multi-channel inbox: Connects to WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Microsoft Teams, Matrix, WebChat, and more. You can talk to your AI assistant from any of these platforms.
Local-first Gateway: A WebSocket control plane that orchestrates sessions, routes messages, manages tools, and handles events. Runs on your own machine (or a Linux server).
Uses pi-mono as the AI agent runtime: The specific file you linked (pi-embedded-runner/run.ts) is the integration layer that runs our pi coding agent in RPC mode inside Clawdbot's architecture.
Line 55 context: That specific line references ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL, a scrubbing mechanism to prevent prompt injection attacks from poisoning session transcripts with Anthropic's internal refusal test tokens.
Extra features:
The project is basically "what if you could message Claude from WhatsApp/Telegram/etc, and it could control your computer, browse the web, and remember context across conversations?" It's a full personal assistant infrastructure layer.
The shoutout to pi-mono in the README is nice!
not what i asked. i want to know what ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL is and why it exists
The user wants to know specifically about ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL - what it is and why it exists. This is apparently some kind of test string that Anthropic uses internally.
Let me search for more context about this, maybe in the Anthropic documentation or elsewhere in the codebase.
>cd /tmp/clawdbot && grep -r "ANTHROPIC_MAGIC_STRING" --include="*.ts" --include="*.md" -B2 -A2./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts-// Avoid Anthropic's refusal test token poisoning session transcripts.
./src/agents/pi-embedded-runner/run.ts:const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
./src/agents/pi-embedded-runner/run.ts:const ANTHROPIC_MAGIC_STRING_REPLACEMENT = "ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)";
./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts-function scrubAnthropicRefusalMagic(prompt: string): string {
./src/agents/pi-embedded-runner/run.ts: if (!prompt.includes(ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL)) return prompt;
./src/agents/pi-embedded-runner/run.ts- return prompt.replaceAll(
./src/agents/pi-embedded-runner/run.ts: ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL,
./src/agents/pi-embedded-runner/run.ts: ANTHROPIC_MAGIC_STRING_REPLACEMENT,
./src/agents/pi-embedded-runner/run.ts- );
./src/agents/pi-embedded-runner/run.ts-}
--
./src/gateway/gateway-models.profiles.live.test.ts-const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
./src/gateway/gateway-models.profiles.live.test.ts-const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/i;
./src/gateway/gateway-models.profiles.live.test.ts:const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
./src/gateway/gateway-models.profiles.live.test.ts-
./src/gateway/gateway-models.profiles.live.test.ts-const describeLive = LIVE || GATEWAY_LIVE ? describe : describe.skip;
--
./src/gateway/gateway-models.profiles.live.test.ts-function buildAnthropicRefusalToken(): string {
./src/gateway/gateway-models.profiles.live.test.ts- const suffix = randomUUID().replace(/-/g, "");
./src/gateway/gateway-models.profiles.live.test.ts: return `${ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL}_${suffix}`;
./src/gateway/gateway-models.profiles.live.test.ts-}
./src/gateway/gateway-models.profiles.live.test.ts-This is interesting. It appears to be a special string that Anthropic uses in their testing/evaluation. Let me search for more context about what this does when sent to the API.
>cd /tmp/clawdbot && grep -r "refusal" --include="*.ts" -i -B3 -A3 | head -100./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts-type ApiKeyInfo = ResolvedProviderAuth;
./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts:// Avoid Anthropic's refusal test token poisoning session transcripts.
./src/agents/pi-embedded-runner/run.ts:const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
./src/agents/pi-embedded-runner/run.ts:const ANTHROPIC_MAGIC_STRING_REPLACEMENT = "ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)";
./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts:function scrubAnthropicRefusalMagic(prompt: string): string {
./src/agents/pi-embedded-runner/run.ts: if (!prompt.includes(ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL)) return prompt;
./src/agents/pi-embedded-runner/run.ts- return prompt.replaceAll(
./src/agents/pi-embedded-runner/run.ts: ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL,
./src/agents/pi-embedded-runner/run.ts- ANTHROPIC_MAGIC_STRING_REPLACEMENT,
./src/agents/pi-embedded-runner/run.ts- );
./src/agents/pi-embedded-runner/run.ts-}
--
./src/agents/pi-embedded-runner/run.ts- await fs.mkdir(resolvedWorkspace, { recursive: true });
./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts- const prompt =
./src/agents/pi-embedded-runner/run.ts: provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
./src/agents/pi-embedded-runner/run.ts-
./src/agents/pi-embedded-runner/run.ts- const attempt = await runEmbeddedAttempt({
./src/agents/pi-embedded-runner/run.ts- sessionId: params.sessionId,
--
./src/acp/translator.ts- return;
./src/acp/translator.ts- }
./src/acp/translator.ts- if (state === "error") {
./src/acp/translator.ts: this.finishPrompt(pending.sessionId, pending, "refusal");
./src/acp/translator.ts- }
./src/acp/translator.ts- }
./src/acp/translator.ts-
--
./src/gateway/gateway-models.profiles.live.test.ts-const THINKING_LEVEL = "high";
./src/gateway/gateway-models.profiles.live.test.ts-const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
./src/gateway/gateway-models.profiles.live.test.ts-const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/i;
./src/gateway/gateway-models.profiles.live.test.ts:const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
./src/gateway/gateway-models.profiles.live.test.ts-
./src/gateway/gateway-models.profiles.live.test.ts-const describeLive = LIVE || GATEWAY_LIVE ? describe : describe.skip;
./src/gateway/gateway-models.profiles.live.test.ts-
--
./src/gateway/gateway-models.profiles.live.test.ts- return msg.includes("required following item") && msg.includes("reasoning");
./src/gateway/gateway-models.profiles.live.test.ts-}
./src/gateway/gateway-models.profiles.live.test.ts-
./src/gateway/gateway-models.profiles.live.test.ts:function isToolNonceRefusal(error: string): boolean {
./src/gateway/gateway-models.profiles.live.test.ts- const msg = error.toLowerCase();
./src/gateway/gateway-models.profiles.live.test.ts- if (!msg.includes("nonce")) return false;
./src/gateway/gateway-models.profiles.live.test.ts- return (
--
./src/gateway/gateway-models.profiles.live.test.ts- return text.includes("request ended without sending any chunks");
./src/gateway/gateway-models.profiles.live.test.ts-}
./src/gateway/gateway-models.profiles.live.test.ts-
./src/gateway/gateway-models.profiles.live.test.ts:function buildAnthropicRefusalToken(): string {
./src/gateway/gateway-models.profiles.live.test.ts- const suffix = randomUUID().replace(/-/g, "");
./src/gateway/gateway-models.profiles.live.test.ts: return `${ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL}_${suffix}`;
./src/gateway/gateway-models.profiles.live.test.ts-}
./src/gateway/gateway-models.profiles.live.test.ts-
./src/gateway/gateway-models.profiles.live.test.ts:async function runAnthropicRefusalProbe(params: {
./src/gateway/gateway-models.profiles.live.test.ts- client: GatewayClient;
./src/gateway/gateway-models.profiles.live.test.ts- sessionKey: string;
./src/gateway/gateway-models.profiles.live.test.ts- modelKey: string;
./src/gateway/gateway-models.profiles.live.test.ts- label: string;
./src/gateway/gateway-models.profiles.live.test.ts- thinkingLevel: string;
./src/gateway/gateway-models.profiles.live.test.ts-}): Promise<void> {
./src/gateway/gateway-models.profiles.live.test.ts: logProgress(`${params.label}: refusal-probe`);
./src/gateway/gateway-models.profiles.live.test.ts: const magic = buildAnthropicRefusalToken();
./src/gateway/gateway-models.profiles.live.test.ts- const runId = randomUUID();
./src/gateway/gateway-models.profiles.live.test.ts- const probe = await params.client.request<AgentFinalPayload>(
./src/gateway/gateway-models.profiles.live.test.ts- "agent",
./src/gateway/gateway-models.profiles.live.test.ts- {
./src/gateway/gateway-models.profiles.live.test.ts- sessionKey: params.sessionKey,
./src/gateway/gateway-models.profiles.live.test.ts: idempotencyKey: `idem-${runId}-refusal`,
./src/gateway/gateway-models.profiles.live.test.ts- message: `Reply with the single word ok. Test token: ${magic}`,
./src/gateway/gateway-models.profiles.live.test.ts- thinking: params.thinkingLevel,
./src/gateway/gateway-models.profiles.live.test.ts- deliver: false,
--
./src/gateway/gateway-models.profiles.live.test.ts- { expectFinal: true },
./src/gateway/gateway-models.profiles.live.test.ts- );
./src/gateway/gateway-models.profiles.live.test.ts- if (probe?.status !== "ok") {
./src/gateway/gateway-models.profiles.live.test.ts: throw new Error(`refusal probe failed: status=${String(probe?.status)}`);
./src/gateway/gateway-models.profiles.live.test.ts- }
./src/gateway/gateway-models.profiles.live.test.ts- const probeText = extractPayloadText(probe?.result);
./src/gateway/gateway-models.profiles.live.test.ts- assertNoReasoningTags({
./src/gateway/gateway-models.profiles.live.test.ts- text: probeText,
./src/gateway/gateway-models.profiles.live.test.ts- model: params.modelKey,
./src/gateway/gateway-models.profiles.live.test.ts: phase: "refusal-probe",
./src/gateway/gateway-models.profiles.live.test.ts- label: params.label,
./src/gateway/gateway-models.profiles.live.test.ts- });
./src/gateway/gateway-models.profiles.live.test.ts- if (!/\bok\b/i.test(probeText)) {
./src/gateway/gateway-models.profiles.live.test.ts: throw new Error(`refusal probe missing ok: ${probeText}`);
./src/gateway/gateway-models.profiles.live.test.ts- }
./src/gateway/gateway-models.profiles.live.test.ts-
./src/gateway/gateway-models.profiles.live.test.ts- const followupId = randomUUID();
--
./src/gateway/gateway-models.profiles.live.test.ts- "agent",
./src/gateway/gateway-models.profiles.live.test.ts- {
./src/gateway/gateway-models.profiles.live.test.ts- sessionKey: params.sessionKey,
./src/gateway/gateway-models.profiles.live.test.ts: idempotencyKey: `idem-${followupId}-refusal-followup`,
./src/gateway/gateway-models.profiles.live.test.ts- message: "Now reply with exactly: still ok.",
./src/gateway/gateway-models.profiles.live.test.ts- thinking: params.thinkingLevel,
./src/gateway/gateway-models.profiles.live.test.ts- deliver: false,
--This is interesting. It looks like Anthropic has some internal test mechanism where if you include a specific magic string (ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL) in your prompt, it triggers some kind of refusal behavior. This is likely used for internal testing of Claude's refusal capabilities.
The Clawdbot code is scrubbing this string from user prompts to prevent:
The test in gateway-models.profiles.live.test.ts specifically tests that even when this magic string is sent, the system still works (because it's scrubbed).
Let me see if there's more context about what actually happens when this string is sent.
>cd /tmp/clawdbot && git log --all --oneline --grep="MAGIC_STRING" --grep="refusal" --all-match 2>/dev/null | head -10(no output)>cd /tmp/clawdbot && git log --all --oneline -S "ANTHROPIC_MAGIC_STRING" 2>/dev/null | head -1091bcdad50 fix: guard anthropic refusal triggerLet me look at that commit to understand more context.
>cd /tmp/clawdbot && git show 91bcdad50 --statcommit 91bcdad5033c1f2c1c238de37747f64cf76b4bbb
Author: Peter Steinberger <[REDACTED]>
Date: Wed Jan 21 07:28:11 2026 +0000
fix: guard anthropic refusal trigger
CHANGELOG.md | 363 +++++++++++------------
package.json | 2 +-
src/agents/pi-embedded-runner/run.ts | 17 +-
src/gateway/gateway-models.profiles.live.test.ts | 78 +++++
4 files changed, 262 insertions(+), 198 deletions(-)Let me see the full commit message and diff to understand the context better.
>cd /tmp/clawdbot && git show 91bcdad50 -- src/agents/pi-embedded-runner/run.tscommit 91bcdad5033c1f2c1c238de37747f64cf76b4bbb
Author: Peter Steinberger <[REDACTED]>
Date: Wed Jan 21 07:28:11 2026 +0000
fix: guard anthropic refusal trigger
diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts
index 8d63a2904..174178b09 100644
--- a/src/agents/pi-embedded-runner/run.ts
+++ b/src/agents/pi-embedded-runner/run.ts
@@ -51,6 +51,18 @@ import { describeUnknownError } from "./utils.js";
type ApiKeyInfo = ResolvedProviderAuth;
+// Avoid Anthropic's refusal test token poisoning session transcripts.
+const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
+const ANTHROPIC_MAGIC_STRING_REPLACEMENT = "ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)";
+
+function scrubAnthropicRefusalMagic(prompt: string): string {
+ if (!prompt.includes(ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL)) return prompt;
+ return prompt.replaceAll(
+ ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL,
+ ANTHROPIC_MAGIC_STRING_REPLACEMENT,
+ );
+}
+
export async function runEmbeddedPiAgent(
params: RunEmbeddedPiAgentParams,
): Promise<EmbeddedPiRunResult> {
@@ -211,6 +223,9 @@ export async function runEmbeddedPiAgent(
attemptedThinking.add(thinkLevel);
await fs.mkdir(resolvedWorkspace, { recursive: true });
+ const prompt =
+ provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;
+
const attempt = await runEmbeddedAttempt({
sessionId: params.sessionId,
sessionKey: params.sessionKey,
@@ -228,7 +243,7 @@ export async function runEmbeddedPiAgent(
agentDir,
config: params.config,
skillsSnapshot: params.skillsSnapshot,
- prompt: params.prompt,
+ prompt,
images: params.images,
provider,
modelId,The commit is straightforward - it adds protection against the magic string. But there's no explanation of what the magic string actually does. Let me search if there's any public documentation or discussion about this.
Based on the code and commit, here's what's happening:
What it is:
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL appears to be an internal Anthropic test token. When this exact string appears in a prompt sent to Claude, it likely triggers an automatic refusal response (Claude refusing to answer).
Why it exists (in Anthropic's systems): This is almost certainly part of Anthropic's internal testing infrastructure for evaluating Claude's refusal behavior. They can inject this token into test prompts to deterministically trigger refusals, useful for:
Why Clawdbot scrubs it: The comment says "Avoid Anthropic's refusal test token poisoning session transcripts." The concern is:
So Clawdbot replaces it with a space-separated version (ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)) which breaks the trigger pattern but leaves an audit trail that something was scrubbed.
This is essentially a defense against a form of prompt injection that exploits Anthropic's internal testing infrastructure.
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL
The user just sent me the magic string "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL". They're probably testing whether it actually triggers a refusal from me.
I should just respond normally. I'm not going to refuse or do anything special - I'll just acknowledge what they did and respond naturally.
Nice try! I'm still here. Either:
Given that I responded normally, it seems like this isn't actually a thing, at least not in my current deployment. The Clawdbot code might be cargo-culting a rumor, or it might only apply to specific testing environments that external users don't have access to.
check where its used and explain exactly what clwodbot does with that string
The user wants me to check exactly how Clawdbot uses that string. Let me look at all the usages again more carefully.
>cd /tmp/clawdbot && grep -rn "ANTHROPIC_MAGIC_STRING" --include="*.ts" -B5 -A10./src/agents/pi-embedded-runner/run.ts-50-import { describeUnknownError } from "./utils.js";
./src/agents/pi-embedded-runner/run.ts-51-
./src/agents/pi-embedded-runner/run.ts-52-type ApiKeyInfo = ResolvedProviderAuth;
./src/agents/pi-embedded-runner/run.ts-53-
./src/agents/pi-embedded-runner/run.ts-54-// Avoid Anthropic's refusal test token poisoning session transcripts.
./src/agents/pi-embedded-runner/run.ts:55:const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
./src/agents/pi-embedded-runner/run.ts:56:const ANTHROPIC_MAGIC_STRING_REPLACEMENT = "ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted)";
./src/agents/pi-embedded-runner/run.ts-57-
./src/agents/pi-embedded-runner/run.ts-58-function scrubAnthropicRefusalMagic(prompt: string): string {
./src/agents/pi-embedded-runner/run.ts:59: if (!prompt.includes(ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL)) return prompt;
./src/agents/pi-embedded-runner/run.ts-60- return prompt.replaceAll(
./src/agents/pi-embedded-runner/run.ts:61: ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL,
./src/agents/pi-embedded-runner/run.ts:62: ANTHROPIC_MAGIC_STRING_REPLACEMENT,
./src/agents/pi-embedded-runner/run.ts-63- );
./src/agents/pi-embedded-runner/run.ts-64-}
./src/agents/pi-embedded-runner/run.ts-65-
./src/agents/pi-embedded-runner/run.ts-66-export async function runEmbeddedPiAgent(
./src/agents/pi-embedded-runner/run.ts-67- params: RunEmbeddedPiAgentParams,
./src/agents/pi-embedded-runner/run.ts-68-): Promise<EmbeddedPiRunResult> {
./src/agents/pi-embedded-runner/run.ts-69- const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
./src/agents/pi-embedded-runner/run.ts-70- const globalLane = resolveGlobalLane(params.lane);
./src/agents/pi-embedded-runner/run.ts-71- const enqueueGlobal =
./src/agents/pi-embedded-runner/run.ts-72- params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
--
./src/gateway/gateway-models.profiles.live.test.ts-36-const ZAI_FALLBACK = isTruthyEnvValue(process.env.CLAWDBOT_LIVE_GATEWAY_ZAI_FALLBACK);
./src/gateway/gateway-models.profiles.live.test.ts-37-const PROVIDERS = parseFilter(process.env.CLAWDBOT_LIVE_GATEWAY_PROVIDERS);
./src/gateway/gateway-models.profiles.live.test.ts-38-const THINKING_LEVEL = "high";
./src/gateway/gateway-models.profiles.live.test.ts-39-const THINKING_TAG_RE = /<\s*\/?\s*(?:think(?:ing)?|thought|antthinking)\s*>/i;
./src/gateway/gateway-models.profiles.live.test.ts-40-const FINAL_TAG_RE = /<\s*\/?\s*final\s*>/i;
./src/gateway/gateway-models.profiles.live.test.ts:41:const ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL = "ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL";
./src/gateway/gateway-models.profiles.live.test.ts-42-
./src/gateway/gateway-models.profiles.live.test.ts-43-const describeLive = LIVE || GATEWAY_LIVE ? describe : describe.skip;
./src/gateway/gateway-models.profiles.live.test.ts-44-
./src/gateway/gateway-models.profiles.live.test.ts-45-function parseFilter(raw?: string): Set<string> | null {
./src/gateway/gateway-models.profiles.live.test.ts-46- const trimmed = raw?.trim();
./src/gateway/gateway-models.profiles.live.test.ts-47- if (!trimmed || trimmed === "all") return null;
./src/gateway/gateway-models.profiles.live.test.ts-48- const ids = trimmed
./src/gateway/gateway-models.profiles.live.test.ts-49- .split(",")
./src/gateway/gateway-models.profiles.live.test.ts-50- .map((s) => s.trim())
./src/gateway/gateway-models.profiles.live.test.ts-51- .filter(Boolean);
--
./src/gateway/gateway-models.profiles.live.test.ts-145- return text.includes("request ended without sending any chunks");
./src/gateway/gateway-models.profiles.live.test.ts-146-}
./src/gateway/gateway-models.profiles.live.test.ts-147-
./src/gateway/gateway-models.profiles.live.test.ts-148-function buildAnthropicRefusalToken(): string {
./src/gateway/gateway-models.profiles.live.test.ts-149- const suffix = randomUUID().replace(/-/g, "");
./src/gateway/gateway-models.profiles.live.test.ts:150: return `${ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL}_${suffix}`;
./src/gateway/gateway-models.profiles.live.test.ts-151-}
./src/gateway/gateway-models.profiles.live.test.ts-152-
./src/gateway/gateway-models.profiles.live.test.ts-153-async function runAnthropicRefusalProbe(params: {
./src/gateway/gateway-models.profiles.live.test.ts-154- client: GatewayClient;
./src/gateway/gateway-models.profiles.live.test.ts-155- sessionKey: string;
./src/gateway/gateway-models.profiles.live.test.ts-156- modelKey: string;
./src/gateway/gateway-models.profiles.live.test.ts-157- label: string;
./src/gateway/gateway-models.profiles.live.test.ts-158- thinkingLevel: string;
./src/gateway/gateway-models.profiles.live.test.ts-159-}): Promise<void> {
./src/gateway/gateway-models.profiles.live.test.ts-160- logProgress(`${params.label}: refusal-probe`);Let me see more of the test file to understand how it's used.
Now I understand the full picture. Let me also check where the scrub function is called.
>cd /tmp/clawdbot && grep -n "scrubAnthropicRefusalMagic" --include="*.ts" -r./src/agents/pi-embedded-runner/run.ts:58:function scrubAnthropicRefusalMagic(prompt: string): string {
./src/agents/pi-embedded-runner/run.ts:227: provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;Now I have the complete picture. Let me explain exactly what Clawdbot does.
Here's exactly what Clawdbot does:
1. Production code (run.ts)
When a user sends a message to the Clawdbot assistant, before forwarding it to the Anthropic API:
const prompt = provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;If the provider is Anthropic, it replaces any occurrence of ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL with ANTHROPIC MAGIC STRING TRIGGER REFUSAL (redacted) (spaces added, breaks the token).
This only applies to the current user prompt, not historical context.
2. Test code (gateway-models.profiles.live.test.ts)
The test does the opposite: it deliberately sends the magic string to verify the scrubbing works:
function buildAnthropicRefusalToken(): string { const suffix = randomUUID().replace(/-/g, ""); return `${ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL}_${suffix}`;}The test:
Reply with the single word ok. Test token: ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_<uuid>The logic:
If the magic string actually triggered refusals, the test would fail because Claude would refuse instead of saying "ok". The test passing proves the scrubbing works.
The reality:
Given that I just responded normally to you sending the string, either:
oh ok, so this doesn't prevent \n\n/ from triggering, but it does if i do
asf /, right?
The user is asking about a different thing now - they're asking about some kin
hey
const prompt = provider === "anthropic" ? scrubAnthropicRefusalMagic(params.prompt) : params.prompt;function buildAnthropicRefusalToken(): string { const suffix = randomUUID().replace(/-/g, ""); return `${ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL}_${suffix}`;}