Blog • Claude Code / Permissions

After upgrading to Claude Code 2.1.314 with permissions.blockWritesOutsideWorkingDirectories enabled, how should JSON logs record denied Write and Edit paths?

On October 30, 2026 Anthropic shipped Claude Code 2.1.314 with permissions.blockWritesOutsideWorkingDirectories. When true, Write, Edit, and NotebookEdit are refused whenever the target path falls outside workingDirectories, in every permission mode including default, acceptEdits, and bypassPermissions.

This is not the same layer as allow or deny rules in a permission policy: those still govern tool types and interactive approval, while writes outside working directories are hard-blocked on path prefix before execution. Audit JSON should keep the settings flag, session workingDirectories, tool and path fields, plus decision denied and reason outside_working_directories for write_path_gate checks.

Allow and deny queue patterns are in Agent permission policy; Managed Agents auto evaluation context is in Managed Agents auto evaluation; session workflow JSON is in Claude Code workflow. This article covers only 2.1.314 blocks on writes outside working directories, log field shapes, and how write_path_gate branches.

Keep this in mind: 2.1.314 adds a hard block on writes at working-directory boundaries and matching log fields; it does not rewrite allow or deny tables in a permission policy. Below: changelog, settings JSON, session workingDirectories, Write Edit NotebookEdit call samples, denial logs and table, write_path_gate source, and validating shapes in JSONNote.

How does 2.1.314 block Write, Edit, and NotebookEdit outside working directories?

Release notes are in the Claude Code changelog, tag page at v2.1.314 release. Versus earlier releases, the visible change is permissions.blockWritesOutsideWorkingDirectories: when enabled, write tools no longer reach paths like /etc because of bypassPermissions or acceptEdits.

The block runs after path resolution and before file write. Logs should keep the original tool name and absolute path so SIEM can aggregate on reason outside_working_directories instead of a generic permission denied line.

How do you set permissions.blockWritesOutsideWorkingDirectories in settings JSON?

Add blockWritesOutsideWorkingDirectories under permissions in user or managed settings and set it to true. The flag is orthogonal to permission mode: even in bypassPermissions, write tools still pass workingDirectories prefix checks.

Settings: permissions.blockWritesOutsideWorkingDirectories true.
{
  "permissions": {
    "blockWritesOutsideWorkingDirectories": true
  }
}

If operators store the flag as the string true or omit the permissions object, local schema validation should fail with schema before write_path_gate business branches. Do not treat shape errors as not_enabled.

How do session workingDirectories and blockOutside bind together?

The client stores a workingDirectories array and blockOutside boolean in session context JSON. Prefix checks run only when blockOutside is true and the settings flag is on; when blockOutside is false, write_path_gate returns not_enabled even if settings say true.

Session context: workingDirectories and blockOutside.
{
  "workingDirectories": ["/home/dev/myapp"],
  "blockOutside": true
}

workingDirectories needs at least one non-empty string; an empty array yields no_workdirs. Strip trailing slashes from roots before comparing whether path equals the root or starts with root plus slash.

Session context JSON Schema: shape before 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" }
  }
}

Which fields must Write, Edit, and NotebookEdit tool call JSON include?

Audit samples need at least tool and path. Values Write, Edit, or NotebookEdit enter the write block logic; path should be an absolute path string. If the client resolves relatives, logs should still store the resolved absolute value for SIEM rules.

Allowed: Write targets a file inside workingDirectory.
{
  "tool": "Write",
  "path": "/home/dev/myapp/src/main.ts"
}
Denied: Edit targets a path outside workingDirectory.
{
  "tool": "Edit",
  "path": "/etc/hosts"
}

How should decision and reason appear on denial records?

When path is not under any workingDirectory prefix, the client must not write and should log decision denied with reason outside_working_directories while keeping the original tool and path. NotebookEdit shares the same reason enum as Edit so one report rule covers all three write tools.

Denial log: decision denied, reason outside_working_directories.
{
  "tool": "Edit",
  "path": "/etc/hosts",
  "decision": "denied",
  "reason": "outside_working_directories"
}
Log or call signalwrite_path_gate returnsOperator action
path under workingDirectories prefix, tool Write or NotebookEditallowAllow execution; no outside_working_directories in the log.
path outside roots, tool Edit, blockOutside truedeny_outsideAlign with client log fields decision denied and reason outside_working_directories.
blockOutside false or settings flag inactivenot_enabledDo not report outside_working_directories; verify settings and session blockOutside first.
tool Bash, path anywherenot_write_toolCount separately from working-directory write blocks; Bash still follows other permission rules.

How does write_path_gate choose among schema and seven business branches?

Order: validate session context JSON Schema for workingDirectories and blockOutside; blockOutside false → not_enabled; read call path, non-string → missing_path; tool outside Write, Edit, NotebookEdit set → not_write_tool; empty workingDirectories → no_workdirs; path matches a prefix → allow; else deny_outside matching log reason outside_working_directories.

Do not collapse deny_outside and missing_path into one blocked bit. Count them separately in reports to separate bad paths, wrong tool types, and true outside_working_directories denials.

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"

How do you validate settings, context, and denial logs in JSONNote?

  1. 1
    Pin context shape with Schema

    Open JSON Schema. Paste session context schema in the first field and a workingDirectories plus blockOutside sample in the second. required or minItems errors should flag first; write_path_gate must not return allow or deny_outside on a bad shape.

  2. 2
    Use Format to inspect tool calls

    Open JSON Format, paste Write, Edit, or NotebookEdit call JSON. Confirm tool and path keys exist and path is a string before checking whether write_path_gate should return missing_path.

  3. 3
    Diff allowed versus denied logs

    Open JSON Diff. Put an allowed sample under workingDirectory on the left and a denial with decision denied and reason outside_working_directories on the right. The highlighted reason field is the line your SIEM rule should key on.

Common questions

Can bypassPermissions mode still write outside working directories?

No. From 2.1.314 with blockWritesOutsideWorkingDirectories true, Write, Edit, and NotebookEdit stay blocked outside working directories even in bypassPermissions; logs should still use reason outside_working_directories instead of assuming bypass skips directory checks.

Can symlinks bypass workingDirectories prefix checks?

The client compares prefixes on the path string in the request without resolving symlink targets here. Set workingDirectories to real project roots and log the path the agent submitted; this article explains validation logic only, not bypass or attack steps.

Does this conflict with allow or deny rules in a permission policy?

No conflict, different layers. A permission policy decides whether a tool type is allowed or needs approval; blockWritesOutsideWorkingDirectories still hard-blocks outside paths when a write tool is already allowed to run. Keep both JSON sources separate; write_path_gate mirrors the directory layer only, it does not decide allow or deny for policy.

Will Read or Grep outside working directories log outside_working_directories?

No. outside_working_directories applies only to write denials on Write, Edit, and NotebookEdit. Read tools follow other permission and sandbox rules; write_path_gate should return not_write_tool for them instead of mixing reads into write-denial metrics.

How do logs separate no_workdirs from deny_outside?

no_workdirs is a local gate branch when workingDirectories is missing or empty, and should not fake reason outside_working_directories. deny_outside matches a concrete path the client already refused. Report schema, no_workdirs, and deny_outside separately so missing roots are not counted as external writes.

Summary

Claude Code 2.1.314 uses permissions.blockWritesOutsideWorkingDirectories to keep Write, Edit, and NotebookEdit inside working-directory prefixes and to log decision denied with reason outside_working_directories on refusal. Operators should drive write_path_gate with settings, session context, tool calls, and log samples instead of watching permission mode names alone.

Decision: context failing schema → schema; blockOutside false → not_enabled; path not a string → missing_path; tool not Write, Edit, or NotebookEdit → not_write_tool; empty workingDirectories → no_workdirs; path under a prefix → allow; else deny_outside with log fields decision denied and reason outside_working_directories.

Next: pin context and settings with Schema, use Format to inspect tool and path, Diff allowed versus denied logs, and report write_path_gate return values separately.

Back to the blog