--- a/src/claude.js
+++ b/src/claude.js
@@ -83,20 +83,16 @@
 const IDLE_TIMEOUT_MS = parseInt(process.env.IDLE_TIMEOUT_MS) || 120000; // 2 min idle = dead
 
 /**
- * Map OC reasoning_effort levels to Claude CLI --effort levels.
- * OC sends: "minimal" | "low" | "medium" | "high" | "xhigh"
- * Claude CLI accepts: "low" | "medium" | "high"
+ * Map alvin-bot reasoning_effort levels to the Claude CLI --effort flag.
+ * PATCHED for alvin-bot (2026-06-16): the upstream had an OpenClaw-specific
+ * off-by-one map (low→medium, high→max). alvin-bot's EffortLevel is
+ * "low" | "medium" | "high" | "max", and the CLI accepts low|medium|high|
+ * xhigh|max — so we pass valid levels straight through 1:1.
  */
 function mapEffort(reasoningEffort) {
     if (!reasoningEffort) return null;
-    const map = {
-        'minimal': 'low',
-        'low':     'medium',
-        'medium':  'high',
-        'high':    'max',
-        'xhigh':   'max',
-    };
-    return map[reasoningEffort] || null;
+    const allowed = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
+    return allowed.has(reasoningEffort) ? reasoningEffort : null;
 }
 
 // --- Dynamic auto-scrub for OC detection bypass ---
@@ -238,7 +234,27 @@
 
         // Always disable native tools (CLI flag, not session property)
         args.push('--tools', '');
+
+        // PATCHED for alvin-bot (2026-06-16): load NO setting sources. Without
+        // this, the CLI auto-discovers the host's ~/.claude/CLAUDE.md + memory
+        // and injects them — for alvin-bot's operator that is a ~32KB file
+        // describing a rich MCP/browser tool environment. It (a) bloated input
+        // from ~600 to ~72,000 tokens and (b) confused Claude about which tools
+        // it actually has, making it REFUSE the <tool_call> text protocol
+        // ("that tool isn't available in this environment"). The orchestrator
+        // supplies the only system prompt + tool contract we want; nothing else
+        // should leak in. Empty value = load user/project/local: none.
+        args.push('--setting-sources', '');
 
+        // PATCHED for alvin-bot (2026-06-16): use NO MCP servers. Same class of
+        // bug as setting-sources — the host has Canva/Gmail/computer-use/etc.
+        // MCP servers configured globally; without this the CLI loads all of
+        // them, injecting ~34KB of MCP tool schemas and making Claude believe
+        // "my tools are Canva and Gmail", so it refuses the read_file/run_shell
+        // <tool_call> protocol. --strict-mcp-config + no --mcp-config = zero MCP
+        // servers, so the only tools Claude sees are the ones we inject as text.
+        args.push('--strict-mcp-config');
+
         // Map OC reasoning_effort → Claude CLI --effort
         const effort = mapEffort(reasoningEffort);
         if (effort) {
--- a/src/server.js
+++ b/src/server.js
@@ -3,6 +3,7 @@
 const express = require('express');
 const fs = require('fs');
 const path = require('path');
+const os = require('os');
 const { v4: uuidv4 } = require('uuid');
 const { convertMessages, convertMessagesCompact, extractNewMessages, extractNewUserMessages } = require('./convert');
 const { buildToolInstructions } = require('./tools');
@@ -60,7 +61,11 @@
 }
 
 // --- Persistence ---
-const STATE_FILE = path.join(__dirname, '..', 'state.json');
+// PATCHED for alvin-bot: write state to a WRITABLE dir, not the package dir
+// (the bundled bridge lives inside a possibly read-only global node_modules).
+// The bot passes CLAUDE_BRIDGE_STATE_FILE; fall back to ~/.alvin-bot.
+const STATE_FILE = process.env.CLAUDE_BRIDGE_STATE_FILE
+    || path.join(os.homedir(), '.alvin-bot', 'claude-bridge-state.json');
 
 function saveState() {
     try {
@@ -267,8 +272,48 @@
 
 // Load persisted state (channelMap, responseMap, requestLog, stats, globalActivity)
 loadState();
+
+/**
+ * PATCHED for alvin-bot (2026-06-16): escape raw control chars that appear
+ * INSIDE JSON string values, so a tool_call whose argument contains a literal
+ * newline/tab still parses. Walks the string tracking quote state; only touches
+ * chars inside string literals. Belt-and-suspenders for the text protocol.
+ */
+function repairJsonControlChars(s) {
+    let out = '', inStr = false, esc = false;
+    for (let i = 0; i < s.length; i++) {
+        const ch = s[i];
+        if (esc) { out += ch; esc = false; continue; }
+        if (ch === '\\') { out += ch; esc = true; continue; }
+        if (ch === '"') { inStr = !inStr; out += ch; continue; }
+        if (inStr) {
+            if (ch === '\n') { out += '\\n'; continue; }
+            if (ch === '\r') { out += '\\r'; continue; }
+            if (ch === '\t') { out += '\\t'; continue; }
+        }
+        out += ch;
+    }
+    return out;
+}
 
 /**
+ * PATCHED for alvin-bot (2026-06-16): normalize the arguments object. Claude
+ * does not always nest args under "arguments" — it commonly FLATTENS them to
+ * the top level, e.g. {"name":"python_execute","code":"..."} instead of
+ * {"name":"python_execute","arguments":{"code":"..."}}. Without recovery the
+ * args are silently dropped (the tool runs with {} → python/run_shell error).
+ * Also accept "input"/"parameters" aliases. Recovers every non-reserved key.
+ */
+function extractToolArgs(parsed) {
+    for (const key of ['arguments', 'input', 'parameters']) {
+        const v = parsed[key];
+        if (v && typeof v === 'object' && !Array.isArray(v)) return v;
+    }
+    const { name, arguments: _a, input: _i, parameters: _p, ...rest } = parsed;
+    return rest;
+}
+
+/**
  * Parse <tool_call> blocks from Claude's response text.
  * Returns array of { id, name, arguments } or empty array.
  */
@@ -285,23 +330,27 @@
             continue;
         }
         const jsonText = raw.slice(start, end + 1);
+        let parsed;
         try {
-            const parsed = JSON.parse(jsonText);
-            if (!parsed || typeof parsed.name !== 'string') {
-                console.error(`[parseToolCalls] Invalid tool_call payload: ${jsonText.slice(0, 300)}`);
+            parsed = JSON.parse(jsonText);
+        } catch (err) {
+            // Lenient retry: repair raw control chars inside string values.
+            try {
+                parsed = JSON.parse(repairJsonControlChars(jsonText));
+            } catch (err2) {
+                console.error(`[parseToolCalls] Failed to parse JSON: ${jsonText.slice(0, 300)}`);
                 continue;
             }
-            const args = (parsed.arguments && typeof parsed.arguments === 'object' && !Array.isArray(parsed.arguments))
-                ? parsed.arguments
-                : {};
-            calls.push({
-                id: `call_${uuidv4().slice(0, 8)}`,
-                name: parsed.name,
-                arguments: args,
-            });
-        } catch (err) {
-            console.error(`[parseToolCalls] Failed to parse JSON: ${jsonText.slice(0, 300)}`);
         }
+        if (!parsed || typeof parsed.name !== 'string') {
+            console.error(`[parseToolCalls] Invalid tool_call payload: ${jsonText.slice(0, 300)}`);
+            continue;
+        }
+        calls.push({
+            id: `call_${uuidv4().slice(0, 8)}`,
+            name: parsed.name,
+            arguments: extractToolArgs(parsed),
+        });
     }
     return calls;
 }
@@ -936,8 +985,12 @@
     });
 }
 
-// Serve React dashboard (built files)
-statusApp.use(express.static(path.join(__dirname, '../dashboard/dist')));
+// Serve React dashboard (built files) — only if present. PATCHED for alvin-bot:
+// the bundled slim bridge ships no dashboard, so guard the static mount + skip
+// the SPA fallback below; the /status JSON endpoint still works.
+const DASHBOARD_DIR = path.join(__dirname, '../dashboard/dist');
+const HAS_DASHBOARD = fs.existsSync(DASHBOARD_DIR);
+if (HAS_DASHBOARD) statusApp.use(express.static(DASHBOARD_DIR));
 
 statusApp.get('/status', (req, res) => {
     res.json({
@@ -971,9 +1024,11 @@
     res.json(result);
 });
 
-// SPA fallback — serve index.html for any non-API route
+// SPA fallback — serve index.html for any non-API route, only when the
+// dashboard is present (PATCHED for alvin-bot: the bundled bridge has none).
 statusApp.get('*', (req, res) => {
-    res.sendFile(path.join(__dirname, '../dashboard/dist/index.html'));
+    if (HAS_DASHBOARD) return res.sendFile(path.join(DASHBOARD_DIR, 'index.html'));
+    res.status(404).json({ error: 'no dashboard in this build; use /status for JSON' });
 });
 
 module.exports = { app, statusApp, stats, saveState };
--- a/src/tools.js
+++ b/src/tools.js
@@ -5,6 +5,40 @@
 const GATEWAY_BLOCKED = new Set(['sessions_send', 'sessions_spawn', 'gateway']);
 
 /**
+ * Render a single OpenAI function-calling parameter schema into a compact,
+ * human/Claude-readable signature plus per-arg notes.
+ *
+ * PATCHED for alvin-bot (2026-06-16): the upstream bridge only passed each
+ * tool's name + description to Claude, dropping the JSON-Schema entirely. That
+ * forced Claude to GUESS argument keys/types — unreliable for anything beyond
+ * the obvious (path/command). We now surface the full parameter contract
+ * (names, types, required-ness, descriptions, enums) so Claude emits
+ * arguments that match what alvin-bot's tool-executor actually expects.
+ */
+function renderParams(fn) {
+    const schema = fn.parameters || {};
+    const props = schema.properties || {};
+    const required = Array.isArray(schema.required) ? schema.required : [];
+    const keys = Object.keys(props);
+    if (keys.length === 0) {
+        return { signature: '', notes: [] };
+    }
+    const sigParts = [];
+    const notes = [];
+    for (const k of keys) {
+        const p = props[k] || {};
+        const opt = required.includes(k) ? '' : '?';
+        const type = p.type || 'any';
+        sigParts.push(`${k}${opt}: ${type}`);
+        const bits = [];
+        if (p.description) bits.push(p.description);
+        if (Array.isArray(p.enum)) bits.push(`one of: ${p.enum.join(', ')}`);
+        if (bits.length > 0) notes.push(`    - ${k}: ${bits.join(' — ')}`);
+    }
+    return { signature: sigParts.join(', '), notes };
+}
+
+/**
  * Build tool instructions for the system prompt.
  *
  * In the new architecture, Claude does NOT execute tools.
@@ -21,8 +55,13 @@
         '',
         '## Tool Calling Protocol',
         '',
-        'When you need to use a tool, output EXACTLY this format and then STOP:',
+        'You HAVE access to the tools listed below. They are live, real, and fully',
+        'functional in this environment. An external orchestrator executes them for',
+        'you and returns the real output. This is how you read files, run commands,',
+        'search the web, etc.',
         '',
+        'To use a tool, output EXACTLY this format and then STOP immediately:',
+        '',
         '<tool_call>',
         '{"name": "tool_name", "arguments": {"key": "value"}}',
         '</tool_call>',
@@ -33,24 +72,44 @@
         '{"name": "web_search", "arguments": {"query": "bitcoin price"}}',
         '</tool_call>',
         '<tool_call>',
-        '{"name": "memory_search", "arguments": {"query": "user preferences"}}',
+        '{"name": "read_file", "arguments": {"path": "/etc/hosts"}}',
         '</tool_call>',
         '',
-        'CRITICAL RULES:',
-        '- Do NOT execute tools yourself. Do NOT use Bash, Read, Write, Edit, WebSearch, WebFetch, Glob, Grep, or any native tools.',
-        '- Output <tool_call> blocks and STOP. The orchestrator will execute them and provide results.',
-        '- If you do not need any tools, just respond with your answer directly.',
-        '- The conversation may already contain tool results from previous turns — use them, do not re-request.',
+        'CRITICAL RULES — follow these exactly:',
+        '- NEVER say a tool is "not available", that you "cannot access" a file/URL/',
+        '  command, or ask the user to paste content. If a listed tool can get it,',
+        '  CALL the tool instead. Claiming unavailability is always wrong here.',
+        '- Do NOT execute tools yourself and do NOT use any native Claude Code tools',
+        '  (Bash, Read, Write, Edit, WebSearch, WebFetch, Glob, Grep, Task). Use ONLY',
+        '  the <tool_call> protocol above.',
+        '- After emitting your <tool_call> block(s), STOP your response immediately.',
+        '  Do NOT write "Here are the results", do NOT guess, invent, or fabricate the',
+        '  tool output. The orchestrator runs the tool and gives you the REAL result',
+        '  in the next turn — only then do you continue.',
+        '- Put ALL arguments INSIDE the "arguments" object — never as top-level',
+        '  keys. Correct: {"name":"python_execute","arguments":{"code":"..."}}.',
+        '  Wrong: {"name":"python_execute","code":"..."}.',
+        '- Use the EXACT argument names shown in each tool signature below. Include',
+        '  every required argument (those without a "?").',
+        '- "arguments" must be valid JSON: escape newlines in string values as \\n,',
+        '  tabs as \\t and double-quotes as \\". Never put a raw line break inside a',
+        '  JSON string.',
+        '- If you genuinely need no tools, just answer directly.',
+        '- The conversation may already contain tool results from previous turns —',
+        '  use them, do not re-request the same call.',
         '',
         'Available tools:',
     ];
 
     for (const tool of tools) {
-        const name = tool.function?.name || tool.name;
+        const fn = tool.function || tool;
+        const name = fn.name;
         if (!name) continue;
         if (GATEWAY_BLOCKED.has(name)) continue;
-        const desc = tool.function?.description || tool.description || '';
-        lines.push(`- **${name}**: ${desc}`);
+        const desc = fn.description || '';
+        const { signature, notes } = renderParams(fn);
+        lines.push(`- **${name}**(${signature}): ${desc}`);
+        for (const note of notes) lines.push(note);
     }
 
     return lines.join('\n');
--- a/src/index.js
+++ b/src/index.js
@@ -11,9 +11,11 @@
     console.log(`[openclaw-claude-bridge] API     → http://127.0.0.1:${API_PORT}`);
 });
 
-// Status server — all interfaces (LAN access for dashboard)
-const statusServer = http.createServer(statusApp).listen(STATUS_PORT, '0.0.0.0', () => {
-    console.log(`[openclaw-claude-bridge] Status  → http://0.0.0.0:${STATUS_PORT}`);
+// Status server — PATCHED for alvin-bot: bind loopback only (was 0.0.0.0).
+// The bundled bridge auto-runs on every user's machine; a status server on all
+// interfaces would expose request history/usage on the LAN. Loopback only.
+const statusServer = http.createServer(statusApp).listen(STATUS_PORT, '127.0.0.1', () => {
+    console.log(`[openclaw-claude-bridge] Status  → http://127.0.0.1:${STATUS_PORT}`);
 });
 
 // --- Graceful shutdown ---
