博客 • Claude Code / 权限

升级到 Claude Code 2.1.314 之后,开启 permissions.blockWritesOutsideWorkingDirectories 时,Write 和 Edit 的路径要在 JSON 日志里怎样记下拒绝?

2026 年 10 月 30 日,Anthropic 发布 Claude Code 2.1.314。新增设置 permissions.blockWritesOutsideWorkingDirectories:当其为 true 时,Write、Edit、NotebookEdit 在 default、acceptEdits、bypassPermissions 等任意 permission 模式下,只要目标 path 落在 workingDirectories 之外,客户端都会直接拒绝执行。

这与 permission policy 里的 allow 或 deny 规则不是同一层:后者仍管工具种类与交互式审批,而工作目录外写入是在 tool 执行前按 path 前缀硬挡。审计 JSON 应同时保留 settings 开关、会话 workingDirectories、tool 与 path 字段,以及 decision denied 与 reason outside_working_directories,便于与本地 write_path_gate 对照。

allow 与 deny 队列写法见 Agent permission policy;Managed Agents 的 auto 评测上下文见 Managed Agents auto 评测;会话级工作流 JSON 见 Claude Code workflow。本篇只写 2.1.314 的工作目录外写入拦截、日志字段形状,以及 write_path_gate 怎样分支。

先记住这一点:2.1.314 改的是工作目录边界上的写入硬挡与对应日志字段,不是重写 permission policy 里的 allow 或 deny 表。下面按 changelog、settings JSON、会话 workingDirectories、Write Edit NotebookEdit 调用样本、拒绝日志与对照表、write_path_gate 源码,以及 JSONNote 里怎样校验形状。

2.1.314 怎样把工作目录外的 Write、Edit、NotebookEdit 一律挡掉?

变更说明见 Claude Code changelog,标签页见 v2.1.314 release。相对更早版本,最显眼的是 permissions.blockWritesOutsideWorkingDirectories:开启后写入类 tool 不再因为 bypassPermissions 或 acceptEdits 而落到 /etc 一类路径上。

拦截发生在 path 解析之后、文件写入之前。日志里应保留原始 tool 名与绝对 path,方便 SIEM 按 reason outside_working_directories 聚合,而不是只记一条泛化的 permission denied。

permissions.blockWritesOutsideWorkingDirectories 在 settings JSON 里怎样写?

在用户或托管 settings 文件的 permissions 对象里加入 blockWritesOutsideWorkingDirectories 并设为 true。该开关与 permission mode 正交:即使会话处于 bypassPermissions,写入类 tool 仍要过 workingDirectories 前缀检查。

settings:permissions.blockWritesOutsideWorkingDirectories 为 true。
{
  "permissions": {
    "blockWritesOutsideWorkingDirectories": true
  }
}

若运维把开关写成字符串 true 或漏掉 permissions 层级,本地 schema 校验应先失败并返回 schema,再进入 write_path_gate 的业务分支。不要把形状错误当成 not_enabled。

会话上下文里 workingDirectories 与 blockOutside 怎样绑定?

客户端在会话上下文 JSON 里写入 workingDirectories 数组与 blockOutside 布尔值。blockOutside 为 true 且 settings 开关已开时,才进入前缀比对;为 false 时 write_path_gate 直接返回 not_enabled,即使 settings 里写了 true。

会话 context:workingDirectories 与 blockOutside。
{
  "workingDirectories": ["/home/dev/myapp"],
  "blockOutside": true
}

workingDirectories 至少一项且每项为非空字符串;空数组会触发 no_workdirs。路径比对前应对根目录去掉尾部斜杠,再判断 path 等于根或以其加斜杠为前缀。

会话 context JSON Schema:先过形状再进 write_path_gate。
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "additionalProperties": false,
  "required": ["workingDirectories", "blockOutside"],
  "properties": {
    "workingDirectories": {
      "type": "array",
      "minItems": 1,
      "items": { "type": "string", "minLength": 1 }
    },
    "blockOutside": { "type": "boolean" }
  }
}

Write、Edit、NotebookEdit 的 tool 调用 JSON 要带哪些字段?

审计样本至少包含 tool 与 path。tool 取值 Write、Edit 或 NotebookEdit 时进入写入拦截逻辑;path 应为绝对路径字符串。相对路径若被客户端解析,日志仍建议写解析后的绝对值,避免 SIEM 规则对不上。

允许:Write 指向 workingDirectory 内文件。
{
  "tool": "Write",
  "path": "/home/dev/myapp/src/main.ts"
}
拒绝:Edit 指向 workingDirectory 外路径。
{
  "tool": "Edit",
  "path": "/etc/hosts"
}

拒绝记录里 decision 与 reason 应怎样写?

当 path 不在任一 workingDirectory 前缀下,客户端不应执行写入,并在 permission 或 tool 日志 JSON 里写 decision denied 与 reason outside_working_directories,同时保留 tool 与 path 原值。NotebookEdit 与 Edit 共用同一 reason 枚举,便于报表一条规则覆盖三种写入 tool。

拒绝日志:decision denied,reason outside_working_directories。
{
  "tool": "Edit",
  "path": "/etc/hosts",
  "decision": "denied",
  "reason": "outside_working_directories"
}
日志或调用现象write_path_gate 返回运维动作
path 在 workingDirectories 前缀内,tool 为 Write 或 NotebookEditallow正常放行,日志无需 outside_working_directories。
path 在根外,tool 为 Edit,blockOutside 为 truedeny_outside对齐客户端日志字段 decision denied 与 reason outside_working_directories。
blockOutside 为 false 或 settings 开关未生效not_enabled不要误报 outside_working_directories;先确认 settings 与会话 blockOutside。
tool 为 Bash,path 指向任意位置not_write_tool与工作目录写入拦截分开统计;Bash 仍受其它 permission 规则约束。

write_path_gate 怎样在 schema 与七条业务分支间选择?

实现顺序:先用会话 context JSON Schema 校验 workingDirectories 与 blockOutside;blockOutside 为 false 返回 not_enabled;读取 call 的 path,非字符串返回 missing_path;tool 不在 Write、Edit、NotebookEdit 集合返回 not_write_tool;workingDirectories 为空返回 no_workdirs;path 匹配任一前缀返回 allow;否则 deny_outside,对应日志 reason outside_working_directories。

不要把 deny_outside 与 missing_path 合成单一 blocked 位。报表分开计数,才能区分路径填错、工具类型不对、以及真正的 outside_working_directories 拒绝。

write_path_gate:schema、not_enabled、missing_path、not_write_tool、no_workdirs、allow、deny_outside。
def actual_type(data: object) -> str:
    if data is None:
        return "null"
    if isinstance(data, bool):
        return "boolean"
    if isinstance(data, list):
        return "array"
    if isinstance(data, int):
        return "integer"
    if isinstance(data, float):
        return "number"
    if isinstance(data, str):
        return "string"
    if isinstance(data, dict):
        return "object"
    raise TypeError(type(data))

def validate(data: object, schema: dict) -> list[str]:
    errors: list[str] = []
    if not isinstance(schema, dict):
        return errors
    types = schema.get("type")
    if isinstance(types, str):
        types = [types]
    actual = actual_type(data)
    if types and not any(t == actual or (t == "number" and actual == "integer") for t in types):
        errors.append("type")
    if "enum" in schema and not any(value == data for value in schema["enum"]):
        errors.append("enum")
    if isinstance(data, str) and "minLength" in schema and len(data) < schema["minLength"]:
        errors.append("minLength")
    if isinstance(data, dict) and schema.get("properties"):
        for key in schema.get("required") or []:
            if key not in data:
                errors.append(f"required:{key}")
        if schema.get("additionalProperties") is False:
            for key in data:
                if key not in schema["properties"]:
                    errors.append(f"additional:{key}")
        for key, sub in schema["properties"].items():
            if key in data and data[key] is not None:
                errors.extend(validate(data[key], sub))
    return errors

def write_path_gate(ctx: dict, call: dict, schema: dict) -> str:
    if validate(ctx, schema):
        return "schema"
    if not ctx.get("blockOutside"):
        return "not_enabled"
    path = call.get("path")
    if not isinstance(path, str):
        return "missing_path"
    tool = call.get("tool")
    if tool not in {"Write", "Edit", "NotebookEdit"}:
        return "not_write_tool"
    roots = ctx.get("workingDirectories") or []
    if not roots:
        return "no_workdirs"
    for root in roots:
        base = root.rstrip("/")
        if path == base or path.startswith(base + "/"):
            return "allow"
    return "deny_outside"

在 JSONNote 里怎样校验 settings、context 与拒绝日志?

  1. 1
    用 Schema 固定 context 形状

    打开 JSON Schema。第一栏贴会话 context schema,第二栏贴 workingDirectories 与 blockOutside 样本。required 或 minItems 错误应先标红,write_path_gate 不应在形状错误时返回 allow 或 deny_outside。

  2. 2
    用格式化核对 tool 调用

    打开 JSON 格式化,贴 Write、Edit 或 NotebookEdit 调用 JSON。确认 tool 与 path 键存在且 path 为字符串,再对照 write_path_gate 是否应走 missing_path。

  3. 3
    用 Diff 对比允许与拒绝日志

    打开 JSON Diff。左边放 workingDirectory 内的允许样本,右边放带 decision denied 与 reason outside_working_directories 的拒绝样本。Diff 高亮的 reason 字段就是 SIEM 规则最该锁定的一行。

常见问题

bypassPermissions 模式还能写到工作目录外吗?

不能。2.1.314 起 blockWritesOutsideWorkingDirectories 为 true 时,Write、Edit、NotebookEdit 在 bypassPermissions 下仍会被 path 前缀挡在工作目录外,日志仍应写 reason outside_working_directories,而不是假设 bypass 跳过目录检查。

符号链接会不会绕过 workingDirectories 前缀?

客户端按请求里的 path 字符串做前缀比对,不在这里展开 symlink 目标路径。运维应把 workingDirectories 设成真实项目根,并在审计里记录 agent 提交的 path;本篇只说明校验逻辑,不提供绕过或攻击步骤。

这与 permission policy 里的 allow 或 deny 规则冲突吗?

不冲突,而是不同层。permission policy 决定某类 tool 默认允许还是要走审批队列;blockWritesOutsideWorkingDirectories 则在已允许执行写入类 tool 时仍按 path 硬挡目录外目标。两套 JSON 应分开保存,write_path_gate 只镜像目录层,不替 policy 做 allow 或 deny 判定。

Read 或 Grep 读工作目录外文件会被记成 outside_working_directories 吗?

不会。outside_working_directories 只对应 Write、Edit、NotebookEdit 的写入拒绝。Read 类 tool 走其它 permission 与 sandbox 规则;本地 write_path_gate 对它们应返回 not_write_tool,不要把读操作混进写入拒绝指标。

no_workdirs 与 deny_outside 在日志里怎样区分?

no_workdirs 是本地闸门在 workingDirectories 缺失或为空时的配置错误分支,通常不应伪造 outside_working_directories reason。deny_outside 才对应客户端已拒绝的具体 path。报表应分别计数 schema、no_workdirs、deny_outside,避免把未配置根目录误当成外泄写入。

总结

Claude Code 2.1.314 用 permissions.blockWritesOutsideWorkingDirectories 把 Write、Edit、NotebookEdit 锁在工作目录前缀内,并在拒绝时把 decision denied 与 reason outside_working_directories 写进 JSON 日志。运维应用 settings、会话 context、tool 调用与日志四套样本驱动 write_path_gate,而不是只盯 permission mode 名称。

直接结论:context 不过 schema 返回 schema;blockOutside 为 false 返回 not_enabled;path 非字符串返回 missing_path;tool 不是 Write、Edit、NotebookEdit 返回 not_write_tool;workingDirectories 为空返回 no_workdirs;path 落在前缀内返回 allow;否则返回 deny_outside,日志对齐 decision denied 与 reason outside_working_directories。

下一步:用 Schema 固定 context 与 settings,用格式化看清 tool 与 path,用 Diff 对比允许与拒绝日志,并在报表里分开统计 write_path_gate 返回值。

返回博客