OpenClaw 报错 Local media path is not under an allowed directory:workspace 白名单与 handler.js 读取路径排查

发布时间:2026/9/27 22:32:52
OpenClaw 报错 Local media path is not under an allowed directory:workspace 白名单与 handler.js 读取路径排查 1. OpenClaw 读取本地图片报错先搞清楚 workspace 边界你在 OpenClaw 里让技能分析本机图片结果日志里蹦出这么一行[tools] image failed: Local media path is not under an allowed directory: C:\Users\Surface\Pictures\expense\支出\CC5FB85C88081C9FBD233A6B75376438.jpg换成 workspace 里的路径照样报[tools] image failed: Local media path is not under an allowed directory: C:\Users\Surface\.openclaw\workspace\expense-1.jpg这个Local media path is not under an allowed directory不是文件不存在也不是权限不够而是 OpenClaw 的一道安全闸门技能skill和钩子hook默认只能读取白名单目录里的文件不能随便摸系统上任意路径。设计初衷很合理——防止某个第三方技能偷偷把你整个磁盘扫一遍。但对刚上手的人来说这道闸门很容易踩因为报错信息只告诉你「不在允许目录」没告诉你「允许目录到底是哪个」。这篇就围绕workspace白名单和handler.js的读取路径把触发条件、配置片段、最小复现和排查步骤一次讲透。适合正在写 OpenClaw 技能、或者用现成技能处理本地图片/账单/文档的人。核心结论先放这报错路径必须落在agents.defaults.workspace指向的目录树内且handler.js里读文件的方式要跟白名单校验逻辑对齐两者缺一不可。2. 前置TaoToken 与 OpenClaw 的模型接入准备OpenClaw 本身是本地网关 技能框架真正干活的模型能力需要接一个兼容接口。我这边习惯用 TaoToken 做模型接入层它的 API 地址是https://taotoken.net/api兼容 OpenAI 风格的调用方式OpenClaw 的模型配置里填上 base URL 和 Key 就能跑。如果你还没配 Key先去控制台建一个控制台入口https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_pathAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path拿到 Key 之后在 OpenClaw 的模型配置里指向https://taotoken.net/api模型名按你实际开通的填。这一步跟本篇的路径报错没有直接因果关系但很多人是在「模型能对话、技能却读不到图」的时候才意识到两套配置是分开的——模型通了不代表文件系统白名单通了。想先验证模型通道是否正常可以用模型对话页发一条测试模型对话https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path如果你打算长期跑编码类或 Agent 类任务Coding Plan 会更省心Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path接入文档在这里配置字段对不上时翻一下接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path3. 可复制配置workspace 白名单与 handler.js 路径校验3.1 先确认当前 workspace 指向哪里OpenClaw 的允许目录基准就是agents.defaults.workspace。先查openclaw config get agents.defaults.workspace典型输出是C:\Users\Surface\.openclaw\workspace。记住这个值后面所有路径判断都以它为根。只要你的图片路径不是这个目录的子路径就会触发Local media path is not under an allowed directory。3.2 方案一把图片放进 workspace最省事不改任何代码直接把素材搬进白名单目录# 1. 在 workspace 下建分类目录 mkdir C:\Users\Surface\.openclaw\workspace\bills\支出 -Force mkdir C:\Users\Surface\.openclaw\workspace\bills\收入 -Force # 2. 复制原图片进 workspace Copy-Item C:\Users\Surface\Pictures\expense\支出\* C:\Users\Surface\.openclaw\workspace\bills\支出\ -Force Copy-Item C:\Users\Surface\Pictures\expense\收入\* C:\Users\Surface\.openclaw\workspace\bills\收入\ -Force # 3. 验证复制结果 dir C:\Users\Surface\.openclaw\workspace\bills\支出 dir C:\Users\Surface\.openclaw\workspace\bills\收入然后重启网关cd C:\Users\Surface\.openclaw .\gateway.cmd在聊天界面默认http://127.0.0.1:18789发生成账单报告 支出C:\Users\Surface\.openclaw\workspace\bills\支出 收入C:\Users\Surface\.openclaw\workspace\bills\收入这条路径完全落在 workspace 内白名单校验直接通过。3.3 方案二扩展 workspace 根目录如果你不想复制文件可以把 workspace 整体指到一个更大的目录# 查看当前配置 openclaw config get agents.defaults.workspace # 新建工作区 $newWorkspace D:\OpenClawWorkspace mkdir $newWorkspace -Force mkdir $newWorkspace\bills\支出 -Force mkdir $newWorkspace\bills\收入 -Force # 迁移原有数据可选 Copy-Item C:\Users\Surface\.openclaw\workspace\* $newWorkspace\ -Recurse -Force # 复制图片 Copy-Item C:\Users\Surface\Pictures\expense\支出\* $newWorkspace\bills\支出\ -Force Copy-Item C:\Users\Surface\Pictures\expense\收入\* $newWorkspace\bills\收入\ -Force # 切换 workspace openclaw config set agents.defaults.workspace $newWorkspace openclaw config get agents.defaults.workspace重启网关后用D:\OpenClawWorkspace\bills\支出这类路径即可。想恢复原配置openclaw config set agents.defaults.workspace C:\Users\Surface\.openclaw\workspace注意改 workspace 会影响所有技能和钩子的可见范围属于全局动作。如果你只是临时处理一批图片方案一更稳。3.4 方案三handler.js 改用 read 工具最规范前面两种是「让路径合规」这一种是「让读取方式合规」。OpenClaw 提供了tools.read它内部会走白名单校验比直接fs.readFile更符合框架设计。在handler.js里加几个辅助函数/** * 用 read 工具校验文件夹路径 */ async function validatePathsWithTool(paths, tools) { const errors []; if (paths.expense) { try { const result await tools.read({ path: paths.expense }); if (!result.isDirectory) { errors.push(支出路径不是文件夹: paths.expense); } } catch (e) { errors.push(支出文件夹不存在或无法访问: paths.expense); } } if (paths.income) { try { const result await tools.read({ path: paths.income }); if (!result.isDirectory) { errors.push(收入路径不是文件夹: paths.income); } } catch (e) { errors.push(收入文件夹不存在或无法访问: paths.income); } } return { valid: errors.length 0, errors }; } /** * 用 read 工具扫描文件夹 */ async function scanFolderWithTool(folderPath, type, tools) { try { const result await tools.read({ path: folderPath }); if (!result.isDirectory) return []; const files result.children || []; return files .filter(f !f.isDirectory /\.(jpg|jpeg|png|bmp|tiff|webp)$/i.test(f.name)) .map(f ({ filename: f.name, fullPath: path.join(folderPath, f.name), type: type })); } catch (error) { console.error([账单分析] 扫描文件夹失败, folderPath, error); return []; } } /** * 用 read 工具读取图片base64 */ async function readImageWithTool(imagePath, tools) { const result await tools.read({ path: imagePath, encoding: base64 }); return result.data; }主函数里从 context 取 tools并替换原来的调用export default async function handler(event, context) { const { tools } context; // 原来的 validatePaths(paths) 换成 const validation await validatePathsWithTool(paths, tools); // 原来的 scanFolder(...) 换成 const expenses paths.expense ? await scanFolderWithTool(paths.expense, 支出, tools) : []; const incomes paths.income ? await scanFolderWithTool(paths.income, 收入, tools) : []; }读图片时优先走工具失败再回退let fileData; try { fileData await readImageWithTool(fileInfo.fullPath, tools); } catch (readError) { const fileBytes await fs.readFile(fileInfo.fullPath); fileData fileBytes.toString(base64); }注意回退分支里的fs.readFile仍然可能触发白名单报错它只是兜底不是绕过。真正合规的路径还是得落在 workspace 内。3.5 方案四符号链接不复制文件需要管理员权限的 PowerShellNew-Item -ItemType SymbolicLink -Path C:\Users\Surface\.openclaw\workspace\图片 -Target C:\Users\Surface\Pictures\expense dir C:\Users\Surface\.openclaw\workspace\图片之后用C:\Users\Surface\.openclaw\workspace\图片\支出访问。删除链接Remove-Item C:\Users\Surface\.openclaw\workspace\图片符号链接在路径字符串层面落在 workspace 内但实际指向外部目录是否被校验逻辑接受取决于 OpenClaw 版本对链接的解析策略建议实测确认。3.6 方案五环境变量与配置文件管理路径如果路径经常变用环境变量或 JSON 配置集中管理更清爽。启动脚本 $env:EXPENSE_PATH C:\Users\Surface\Pictures\expense cd C:\Users\Surface\.openclaw .\gateway.cmd | Out-File -FilePath C:\Users\Surface\.openclaw\start-gateway.ps1 -Encoding utf8handler.js里读取const EXPENSE_BASE_PATH process.env.EXPENSE_PATH || null; function parsePathsFromInput(text) { const paths { expense: , income: }; if (!text.includes(支出) !text.includes(支出:) EXPENSE_BASE_PATH) { paths.expense path.join(EXPENSE_BASE_PATH, 支出); paths.income path.join(EXPENSE_BASE_PATH, 收入); return paths; } const expenseMatch text.match(/支出[:]\s*([^\s])/); if (expenseMatch) paths.expense expenseMatch[1].replace(/[\\/]$/, ); const incomeMatch text.match(/收入[:]\s*([^\s])/); if (incomeMatch) paths.income incomeMatch[1].replace(/[\\/]$/, ); return paths; }配置文件方式{ expensePath: C:\\Users\\Surface\\Pictures\\expense\\支出, incomePath: C:\\Users\\Surface\\Pictures\\expense\\收入 }const CONFIG_PATH path.join(process.env.USERPROFILE, .openclaw, bill-paths.json); async function loadPathsFromConfig() { try { const data await fs.readFile(CONFIG_PATH, utf8); return JSON.parse(data); } catch (e) { return null; } }这两种方式解决的是「路径从哪来」不解决「路径是否在白名单内」。配置里的路径最终仍要落在 workspace 下否则报错照旧。4. 验证请求最小复现与成功结果4.1 最小复现想稳定复现Local media path is not under an allowed directory用一条 workspace 外的路径即可生成账单报告 支出C:\Users\Surface\Pictures\expense\支出 收入C:\Users\Surface\Pictures\expense\收入日志会打印image failed: Local media path is not under an allowed directory: ...。把同一批图片复制进 workspace 后再发生成账单报告 支出C:\Users\Surface\.openclaw\workspace\bills\支出 收入C:\Users\Surface\.openclaw\workspace\bills\收入4.2 成功结果长什么样技能正常执行时日志里不再出现image failed而是逐张图片的 OCR/分析记录最后输出账单汇总。你可以用一条命令快速确认文件确实在允许目录内$ws (openclaw config get agents.defaults.workspace).Trim() $target C:\Users\Surface\.openclaw\workspace\bills\支出\test.jpg if ($target.StartsWith($ws, [System.StringComparison]::OrdinalIgnoreCase)) { Write-Host 路径在白名单内 -ForegroundColor Green } else { Write-Host 路径越界会触发报错 -ForegroundColor Red }这个前缀判断就是白名单校验的核心逻辑目标路径必须以 workspace 为前缀。大小写、斜杠方向、末尾多余分隔符都可能影响判断所以handler.js里做路径拼接时建议统一用path.join而不是手写字符串。4.3 用 read 工具做一次自检在技能里加一段临时日志确认tools.read能拿到目录const probe await tools.read({ path: paths.expense }); console.log([自检] isDirectory , probe.isDirectory, children , (probe.children || []).length);如果这里就抛异常说明路径本身越界跟后面的 OCR 逻辑无关先回去改路径。5. 本篇常见错排查5.1 路径明明在 workspace 下为什么还报错最常见的原因是字符串拼接出了隐藏字符。比如从聊天输入里正则提取路径时末尾带了空格或换行// 错误没清理尾部空白 paths.expense expenseMatch[1]; // 正确去掉尾部空白和多余分隔符 paths.expense expenseMatch[1].trim().replace(/[\\/]$/, );另一个原因是用了相对路径。.\bills\支出这种写法在不同工作目录下解析结果不同白名单校验拿到的绝对路径可能已经越界。统一转绝对路径const path require(path); paths.expense path.resolve(paths.expense);5.2 改了 workspace 配置但没生效openclaw config set之后必须重启网关配置才会被重新加载。另外确认你改的是agents.defaults.workspace而不是某个技能自己的局部配置——局部配置可能覆盖全局值。openclaw config get agents.defaults.workspace如果输出还是旧值说明设置没写进去检查命令里的路径是否含空格未加引号。5.3 handler.js 里 fs.readFile 能读tools.read 却失败这说明路径越界而fs.readFile是 Node 原生能力不受 OpenClaw 白名单约束。能读不代表合规一旦框架在更上层拦截技能照样失败。正确做法是让路径先合规再用tools.read。5.4 符号链接方案时好时坏不同版本对符号链接的解析策略不一致有的会解析真实路径再校验有的只看链接字符串。如果必须用链接测试时多跑几次并确认目标目录本身也在允许范围内。5.5 报错只出现在图片文本文件却正常图片读取走的是 media 通道校验比普通文本读取更严。文本技能可能用了不同的读取入口所以表现不一致。排查时以图片路径为准别被文本正常误导。5.6 排查顺序建议先跑openclaw config get agents.defaults.workspace拿到根目录再用前缀判断脚本确认目标路径是否越界然后检查handler.js里路径提取有没有 trim 和 resolve最后确认读取走的是tools.read还是裸fs。这四步能覆盖九成以上的Local media path is not under an allowed directory。6. 接入与后续把模型通道和文件白名单分开管路径问题解决后如果你还想把模型调用也理顺建议把两件事分开管理文件白名单归 OpenClaw 的 workspace 配置模型通道归 TaoToken 的 API 配置。需要新建或轮换 Key 时走这里API Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path如果你在跑 Claude Code 这类编码 AgentAnthropic 兼容入口的配置可以参考ClaudeCodeAnthropichttps://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path长期做编码或 Agent 任务Coding Plan 的额度模型更适合持续跑Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentopenclaw_media_path最后留一个我踩过的坑路径校验失败时日志里的路径是技能实际拿到的字符串不一定等于你输入框里写的。中间可能经过正则提取、拼接、环境变量替换。排查时先把handler.js里最终传给tools.read的那个变量打印出来跟 workspace 根目录做前缀比对比盯着输入框猜要快得多。