블로그 • Claude Code / 권한

Claude Code 2.1.314로 올리고 permissions.blockWritesOutsideWorkingDirectories를 켰을 때 거부된 Write와 Edit path를 JSON 로그에 어떻게 남기나?

2026년 10월 30일 Anthropic은 Claude Code 2.1.314을 냈다. permissions.blockWritesOutsideWorkingDirectories가 true이면 Write, Edit, NotebookEdit는 default, acceptEdits, bypassPermissions 등 모든 permission 모드에서 workingDirectories 밖 path를 거부한다.

permission policy의 allow·deny 규칙과 같은 층이 아니다. 후자는 도구 종류와 대화형 승인을, 작업 디렉터리 밖 쓰기는 실행 전 path 접두로 막는다. 감사 JSON은 설정 스위치, 세션 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이 working directories 밖 Write, Edit, NotebookEdit를 어떻게 막나

변경 설명은 Claude Code changelog, 태그 페이지 v2.1.314 release. 이전 버전 대비 눈에 띄는 것은 permissions.blockWritesOutsideWorkingDirectories다. 켜면 쓰기 tool이 bypassPermissions나 acceptEdits 때문에 /etc류 path에 닿지 않는다.

차단은 path 해석 후·파일 쓰기 전에 일어난다. 로그는 원래 tool명과 절대 path를 남겨 일반 permission denied가 아니라 reason outside_working_directories로 SIEM 집계하게 한다.

settings JSON에서 permissions.blockWritesOutsideWorkingDirectories는 어떻게 쓰나

사용자 또는托管 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는 어떻게 묶이나

클라이언트는 세션 context JSON에 workingDirectories 배열과 blockOutside 불리언을 쓴다. blockOutside가 true이고 settings 스위치가 켜져 있을 때만 접두 비교한다. false면 settings가 true여도 write_path_gate는 not_enabled.

세션 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이면 쓰기 차단 logic으로 들어간다. path는 절대 경로 문자열이어야 한다. 클라이언트가 상대를 해석하면 로그에는 해석된 절대값을 남겨 SIEM 규칙을 맞춘다.

허용: Write가 workingDirectory 내부 파일을 가리킴.
{
  "tool": "Write",
  "path": "/home/dev/myapp/src/main.ts"
}
거부: Edit가 workingDirectory 밖 path를 가리킴.
{
  "tool": "Edit",
  "path": "/etc/hosts"
}

거부 기록의 decision과 reason 작성법

path가 어떤 workingDirectory 접두에도 없으면 클라이언트는 쓰지 않고 permission 또는 tool 로그 JSON에 decision denied와 reason outside_working_directories를 쓰며 tool과 path 원값을 남긴다. NotebookEdit는 Edit와 같은 reason enum으로 세 쓰기 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_enabledoutside_working_directories로 오보하지 않는다. settings와 세션 blockOutside를 먼저 확인.
tool Bash, path 어디든not_write_tool작업 디렉터리 쓰기 차단과 분리 집계. Bash는 다른 permission 규칙 적용.

write_path_gate가 schema와 일곱 business 분기를 어떻게 고르나

순서: 세션 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 하나로 접지 않는다. 보고서에서 나눠 세어 path 오류, tool 유형 오류, 진짜 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. 1칸에 세션 context schema, 2칸에 workingDirectories와 blockOutside 샘플. required나 minItems 오류를 먼저 표시하고 형태가 깨지면 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 모드에서 working directories 밖에 쓸 수 있나?

아니다. 2.1.314부터 blockWritesOutsideWorkingDirectories가 true이면 bypassPermissions에서도 Write, Edit, NotebookEdit는 working directories 밖에서 path 접두로 막히고 로그는 reason outside_working_directories를 써야 한다. bypass가 디렉터리 검사를 건너뛴다고 가정하지 않는다.

심볼릭 링크가 workingDirectories 접두 검사를 우회하나?

클라이언트는 요청의 path 문자열로 접두를 비교하며 여기서 symlink 대상을 펼치지 않는다. workingDirectories를 실제 프로젝트 루트로 두고 agent가 제출한 path를 감사에 남긴다. 이 글은 검증 logic만 설명하며 bypass나 공격 절차는 다루지 않는다.

permission policy의 allow·deny 규칙과 충돌하나?

충돌이 아니라 다른 층이다. permission policy는 tool 유형의 기본 허용·승인 큐를 정하고 blockWritesOutsideWorkingDirectories는 쓰기 tool 실행이 허용돼도 path로 디렉터리 밖을硬挡한다. JSON은 분리 보관하고 write_path_gate는 디렉터리 층만 반영하며 policy allow·deny는 판정하지 않는다.

Read나 Grep이 working directories 밖을 읽으면 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를 working directory 접두 안에 두고 거부 시 JSON 로그에 decision denied와 reason outside_working_directories를 쓴다. 운영은 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 반환값을 보고서에서 분리 집계.

블로그로 돌아가기