파일 도구: Read, Write, Edit

세 도구, 하나의 계약 — 읽기-쓰기 계약부터 따옴표 정규화, 중복 제거까지.

01

개요: 세 도구, 하나의 계약

파일 도구 시스템은 세 가지 도구로 구성되지만, 하나의 핵심 불변식을 공유합니다:

"모든 쓰기 작업은 대상 파일의 사전 읽기를 요구한다."

세 도구의 능력 비교:

도구 역할 특징
Read 읽기 전용 동시성 안전, 이미지/PDF/Jupyter 지원, 토큰 제한 25,000
Write 전체 내용 교체 기존 파일은 반드시 사전 Read 필요, 매번 전체 파일 전송
Edit 정확한 문자열 치환 차이분만 전송, 따옴표 정규화 지원
02

Read 도구 심층 분석

페이지네이션

Read는 기본적으로 2,000줄을 반환합니다. offsetlimit 파라미터로 특정 범위를 지정할 수 있습니다.

토큰 제한 — 2단계 게이팅

파일 크기 검증은 두 단계로 이루어집니다:

  1. 빠른 대략적 추정: 파일 크기에서 토큰 수를 빠르게 추정
  2. 정확한 토큰 카운트: 초과 시 API를 호출하여 정확한 토큰 수 확인

25,000 토큰을 초과하면 MaxFileReadTokenExceededError가 발생합니다. 잘라내기 대신 오류를 반환하는 것이 핵심 설계 결정입니다 — 잘라내기는 약 25K 토큰의 응답을 생성하지만, 오류는 약 100바이트만 차지합니다.

이미지 지원

PNG, JPG, GIF, WebP를 지원합니다. sharp 또는 image-processor-napi를 통해 선택적으로 리사이즈합니다.

주의사항

macOS 스크린샷 파일명에 포함된 U+202F(Narrow No-Break Space) 공백 문자를 자동으로 감지하고 재시도합니다. 일반 공백으로 경로를 입력하면 파일을 찾지 못하므로, 시스템이 자동으로 U+202F로 대체하여 재시도합니다.

PDF 지원

pages 파라미터로 읽을 페이지 범위를 지정하며, 호출당 최대 20페이지로 제한됩니다.

중복 제거

readFileState에 파일 메타데이터(경로, mtime, 내용 해시)를 저장합니다. 동일 파일을 다시 읽을 때 mtime이 일치하면 전체 내용 대신 약 100바이트의 스텁을 반환합니다.

딥 다이브 — 중복 제거의 영향

A/B 테스트 데이터에 따르면 Read 호출의 약 18%가 동일 파일에 대한 재읽기입니다. 중복 제거는 이 18%의 호출에서 약 25K 토큰(전체 파일 내용) 대신 약 100바이트 스텁을 반환하여 상당한 컨텍스트 윈도우를 절약합니다.

차단 디바이스 경로

/dev/zero, /dev/random, /dev/urandom 등 무한 스트림을 생성하는 디바이스 경로는 차단됩니다. 단, /dev/null은 허용됩니다(빈 내용을 반환).

03

Write 도구 심층 분석

Write는 파일의 전체 내용을 교체합니다. 기존 파일에 대해서는 반드시 사전 Read가 완료되어야 합니다.

3가지 실패 모드

  1. 세션 내 읽기 없음 (errorCode 2): 해당 파일에 대한 Read 호출이 세션에서 한 번도 실행되지 않은 경우
  2. 부분 읽기 (errorCode 2): offset/limit으로 일부만 읽은 후 Write 시도 — isPartialView: true로 설정되어 전체 읽기로 인정되지 않음
  3. 읽기 후 파일 수정됨 (errorCode 3): Read 이후 외부 프로세스가 파일을 수정한 경우 — mtime 비교로 감지

줄 끝 정책

Write는 항상 LF(\n)로 저장합니다. 이전 접근법에서 CRLF를 보존하면 bash 스크립트가 조용히 손상되는 문제가 발견되어, LF 강제 정책으로 변경되었습니다.

원자적 쓰기 시퀀스

Write의 실행 순서는 다음과 같습니다:

  1. mkdir — 대상 디렉토리가 없으면 생성
  2. fileHistoryTrackEdit — 변경 이력 추적
  3. 동기 읽기 + 낡음 검사 — 최종 시점에서 파일이 변경되지 않았는지 확인
  4. 디스크 쓰기 — 실제 파일 작성
// Write 원자적 시퀀스 (간소화)
await mkdir(dirname(filePath), { recursive: true })
fileHistoryTrackEdit(filePath, oldContent)

// 낡음 검사: 마지막 Read 이후 파일이 변경되었는지 확인
const currentStat = statSync(filePath)
if (currentStat.mtimeMs !== readFileState.mtimeMs) {
  throw new ToolError('File modified since last read', { errorCode: 3 })
}

writeFileSync(filePath, normalizedContent)
04

Edit 도구 심층 분석

Edit은 정확한 문자열 치환을 수행하며, 차이분만 전송합니다. 전체 파일 대신 변경할 부분만 보내므로 토큰 효율이 높습니다.

고유성 요구

old_string이 파일 내에서 여러 위치에 일치하면 오류 코드 9가 반환됩니다. 어떤 위치를 교체해야 할지 모호하기 때문입니다. 단, replace_all: true를 설정하면 모든 일치 위치를 교체합니다.

따옴표 정규화

Edit의 가장 정교한 기능 중 하나입니다. 두 단계로 동작합니다:

  1. findActualString(): 파일 내용과 검색 문자열을 모두 직선 따옴표(ASCII)로 정규화하여 일치 위치를 찾음
  2. preserveQuoteStyle(): new_string에 원본 파일의 둥근 따옴표(curly quotes) 스타일을 적용하여 기존 스타일 보존
// 따옴표 정규화 흐름
// 파일 내용: "Hello, world"  (둥근 따옴표)
// old_string:  "Hello, world"  (직선 따옴표)

// 1단계: findActualString() — 양쪽 모두 직선 따옴표로 정규화
normalize("\u201cHello, world\u201d") → "\"Hello, world\""
normalize("\"Hello, world\"")    → "\"Hello, world\""
// 일치! 원본 위치 기록

// 2단계: preserveQuoteStyle() — new_string에 둥근 따옴표 스타일 적용
// new_string: "Hi, world" → \u201cHi, world\u201d

새 파일 생성

old_string: ""으로 설정하면 비존재 파일을 생성할 수 있습니다. 빈 문자열은 빈 파일에 일치하므로, 새 파일의 전체 내용을 new_string으로 전달합니다.

탈위생화 테이블

정확한 일치가 실패하면 위생화된 문자열을 원래 형태로 되돌리며 재시도합니다:

후행 공백 제거

Edit은 기본적으로 후행 공백을 제거하지만, .md.mdx 파일은 제외됩니다. Markdown에서 줄 끝의 두 칸 공백은 하드 줄바꿈(<br>)을 의미하므로 제거하면 문서 형식이 깨집니다.

05

Write vs Edit 비교

기준 Write Edit
전송량 매번 전체 파일 전송 차이분만 전송 (단일 상수 변경에 약 20자)
적합한 용도 새 파일 생성, 전체 재작성 부분 수정, 변수명 변경, 줄 추가/삭제
따옴표 정규화 미지원 지원 (findActualString + preserveQuoteStyle)
다중 위치 교체 해당 없음 (전체 교체) replace_all: true로 지원

선택 가이드: 기존 파일에서 소수의 줄만 변경할 때는 Edit이 토큰 효율적입니다. 파일의 대부분을 변경하거나 새 파일을 처음부터 작성할 때는 Write가 직관적입니다.

06

읽기-쓰기 계약

읽기-쓰기 계약은 4단계로 실행됩니다:

  1. Read가 readFileState에 저장: 파일 경로, mtime, 내용 해시, 부분 읽기 여부(isPartialView)를 기록
  2. validateInput()이 검사: Write/Edit 호출 시 해당 파일에 대한 readFileState 존재 여부와 부분 읽기 여부를 확인
  3. call()이 원자적 낡음 검사 재실행: 실제 쓰기 직전에 파일의 현재 mtime을 readFileState와 비교하여 중간에 변경되지 않았는지 최종 확인
  4. Write/Edit 실행 및 readFileState 갱신: 쓰기 완료 후 새로운 mtime과 내용으로 readFileState를 업데이트
// 읽기-쓰기 계약 4단계

// 1단계: Read가 상태 저장
readFileState[filePath] = {
  mtimeMs: stat.mtimeMs,
  contentHash: hash(content),
  isPartialView: !!(offset || limit),
}

// 2단계: validateInput()에서 사전 검사
if (!readFileState[filePath]) {
  throw new ToolError('Must read file before writing', { errorCode: 2 })
}
if (readFileState[filePath].isPartialView) {
  throw new ToolError('Partial read insufficient', { errorCode: 2 })
}

// 3단계: call()에서 원자적 낡음 검사
const current = statSync(filePath)
if (current.mtimeMs !== readFileState[filePath].mtimeMs) {
  throw new ToolError('File changed since read', { errorCode: 3 })
}

// 4단계: 쓰기 후 상태 갱신
writeFileSync(filePath, newContent)
readFileState[filePath] = { mtimeMs: statSync(filePath).mtimeMs, ... }
주의사항 — 일반적 함정

린터나 포매터가 파일 저장 시(IDE의 "저장 시 포맷" 기능 등) Claude의 Read와 Write 사이에 파일을 수정하면 errorCode 3이 발생합니다. 사용자가 Claude의 편집과 동시에 파일을 저장하면 mtime이 변경되어 낡음 검사에 걸립니다. 이 경우 Claude는 파일을 다시 Read한 후 Write/Edit을 재시도해야 합니다.

07

핵심 요약

핵심 포인트

  • 세 파일 도구(Read, Write, Edit)는 "모든 쓰기 전에 읽기"라는 단일 불변식을 공유합니다
  • Read의 토큰 제한은 2단계 게이팅으로 구현됩니다 — 25,000 토큰 초과 시 잘라내기 대신 오류를 반환하여 약 25K 토큰을 절약합니다
  • Read의 중복 제거는 mtime 기반으로 동일 파일 재읽기(전체의 약 18%)에서 약 100바이트 스텁만 반환합니다
  • Write는 항상 LF로 저장하여 CRLF로 인한 bash 스크립트 손상을 방지합니다
  • Edit의 따옴표 정규화(findActualString + preserveQuoteStyle)는 직선/둥근 따옴표 불일치를 자동으로 처리합니다
  • Edit의 고유성 요구는 다중 일치 시 모호성을 방지하며, replace_all: true로 우회 가능합니다
  • 읽기-쓰기 계약의 4단계(저장 → 검사 → 낡음 확인 → 갱신)는 동시 수정으로 인한 데이터 손실을 방지합니다
  • .md/.mdx 파일에서는 후행 공백을 유지합니다 — Markdown 하드 줄바꿈을 보존하기 위한 의도적 예외입니다
08

지식 확인

퀴즈 — 5문제

Q1. offset/limit으로 파일의 일부만 Read한 후 Write하면 어떻게 되나요?

  • A) Write가 성공한다
  • B) Write가 실패한다 — 부분 읽기는 isPartialView: true를 설정하므로 전체 읽기로 인정되지 않는다
  • C) 읽은 줄만 업데이트된다
  • D) 1,000줄 초과 파일에서만 실패한다
부분 읽기(offset/limit 사용)는 isPartialView: true로 기록됩니다. Write의 validateInput()은 이를 확인하여 errorCode 2를 반환합니다. 파일 전체를 보지 않고 전체를 교체하는 것은 데이터 손실 위험이 있기 때문입니다.

Q2. Edit이 "3개 일치 발견" 오류를 반환할 때 올바른 해결법은?

  • A) old_string에 더 많은 컨텍스트만 추가한다
  • B) 모두 변경할 경우 replace_all: true를 설정하고, 단일 교체가 필요하면 더 많은 컨텍스트를 추가한다
  • C) Edit 대신 Write를 사용한다
  • D) 파일을 다시 Read한다
다중 일치 시 두 가지 경로가 있습니다. 모든 위치를 동일하게 변경하려면 replace_all: true가 적합합니다. 특정 하나만 변경하려면 old_string에 주변 코드를 더 포함하여 고유하게 만들어야 합니다.

Q3. 256KB Read 게이트를 초과하는 파일에 대해 "잘라내기 대신 오류"로 되돌린 이유는?

  • A) 잘라내기가 데이터 손실을 유발하기 때문
  • B) 잘라내기는 약 25K 토큰의 응답을 소비하지만, 오류는 약 100바이트만 차지하기 때문
  • C) API가 부분 파일 내용을 지원하지 않기 때문
  • D) 줄 끝 변환 문제 때문
잘라내기된 내용을 반환하면 약 25,000 토큰의 컨텍스트 윈도우를 소비하면서도 불완전한 정보를 제공합니다. 오류를 반환하면 약 100바이트만 사용되며, 모델이 offset/limit으로 필요한 부분만 읽거나 다른 전략을 선택할 수 있습니다.

Q4. 둥근 따옴표로 작성된 파일에 직선 따옴표로 old_string을 지정하여 Edit하면?

  • A) 일치하지 않아 실패한다
  • B) 성공한다 — findActualString()이 양쪽을 정규화하여 일치시키고, 둥근 따옴표 스타일이 보존된다
  • C) 둥근 따옴표가 ASCII 직선 따옴표로 교체된다
  • D) replace_all 모드에서만 동작한다
findActualString()은 파일과 검색 문자열을 모두 직선 따옴표로 정규화하여 위치를 찾습니다. 일치가 확인되면 preserveQuoteStyle()new_string에 원본의 둥근 따옴표 패턴을 적용합니다. 사용자가 따옴표 스타일을 몰라도 편집이 가능합니다.

Q5. 중복 제거 시스템이 파일 미변경 시 반환하는 것은?

  • A) 빈 문자열
  • B) 전체 내용을 재전송
  • C) file_unchanged 타입 결과와 짧은 스텁 메시지
  • D) Grep 사용을 제안하는 오류
mtime이 일치하면 file_unchanged 타입의 결과와 약 100바이트의 짧은 스텁 메시지를 반환합니다. 전체 내용을 재전송하지 않으므로 컨텍스트 윈도우를 절약하면서도 모델에게 파일이 변경되지 않았음을 알립니다.
0 / 5