세 도구, 하나의 계약 — 읽기-쓰기 계약부터 따옴표 정규화, 중복 제거까지.
파일 도구 시스템은 세 가지 도구로 구성되지만, 하나의 핵심 불변식을 공유합니다:
"모든 쓰기 작업은 대상 파일의 사전 읽기를 요구한다."
세 도구의 능력 비교:
| 도구 | 역할 | 특징 |
|---|---|---|
Read |
읽기 전용 | 동시성 안전, 이미지/PDF/Jupyter 지원, 토큰 제한 25,000 |
Write |
전체 내용 교체 | 기존 파일은 반드시 사전 Read 필요, 매번 전체 파일 전송 |
Edit |
정확한 문자열 치환 | 차이분만 전송, 따옴표 정규화 지원 |
Read는 기본적으로 2,000줄을 반환합니다. offset과 limit 파라미터로 특정 범위를 지정할 수 있습니다.
파일 크기 검증은 두 단계로 이루어집니다:
25,000 토큰을 초과하면 MaxFileReadTokenExceededError가 발생합니다. 잘라내기 대신 오류를 반환하는 것이 핵심 설계 결정입니다 — 잘라내기는 약 25K 토큰의 응답을 생성하지만, 오류는 약 100바이트만 차지합니다.
PNG, JPG, GIF, WebP를 지원합니다. sharp 또는 image-processor-napi를 통해 선택적으로 리사이즈합니다.
macOS 스크린샷 파일명에 포함된 U+202F(Narrow No-Break Space) 공백 문자를 자동으로 감지하고 재시도합니다. 일반 공백으로 경로를 입력하면 파일을 찾지 못하므로, 시스템이 자동으로 U+202F로 대체하여 재시도합니다.
pages 파라미터로 읽을 페이지 범위를 지정하며, 호출당 최대 20페이지로 제한됩니다.
readFileState에 파일 메타데이터(경로, mtime, 내용 해시)를 저장합니다. 동일 파일을 다시 읽을 때 mtime이 일치하면 전체 내용 대신 약 100바이트의 스텁을 반환합니다.
A/B 테스트 데이터에 따르면 Read 호출의 약 18%가 동일 파일에 대한 재읽기입니다. 중복 제거는 이 18%의 호출에서 약 25K 토큰(전체 파일 내용) 대신 약 100바이트 스텁을 반환하여 상당한 컨텍스트 윈도우를 절약합니다.
/dev/zero, /dev/random, /dev/urandom 등 무한 스트림을 생성하는 디바이스 경로는 차단됩니다. 단, /dev/null은 허용됩니다(빈 내용을 반환).
Write는 파일의 전체 내용을 교체합니다. 기존 파일에 대해서는 반드시 사전 Read가 완료되어야 합니다.
offset/limit으로 일부만 읽은 후 Write 시도 — isPartialView: true로 설정되어 전체 읽기로 인정되지 않음Write는 항상 LF(\n)로 저장합니다. 이전 접근법에서 CRLF를 보존하면 bash 스크립트가 조용히 손상되는 문제가 발견되어, LF 강제 정책으로 변경되었습니다.
Write의 실행 순서는 다음과 같습니다:
mkdir — 대상 디렉토리가 없으면 생성fileHistoryTrackEdit — 변경 이력 추적// 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)
Edit은 정확한 문자열 치환을 수행하며, 차이분만 전송합니다. 전체 파일 대신 변경할 부분만 보내므로 토큰 효율이 높습니다.
old_string이 파일 내에서 여러 위치에 일치하면 오류 코드 9가 반환됩니다. 어떤 위치를 교체해야 할지 모호하기 때문입니다. 단, replace_all: true를 설정하면 모든 일치 위치를 교체합니다.
Edit의 가장 정교한 기능 중 하나입니다. 두 단계로 동작합니다:
findActualString(): 파일 내용과 검색 문자열을 모두 직선 따옴표(ASCII)로 정규화하여 일치 위치를 찾음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으로 전달합니다.
정확한 일치가 실패하면 위생화된 문자열을 원래 형태로 되돌리며 재시도합니다:
<fnr> → <function_results>Edit은 기본적으로 후행 공백을 제거하지만, .md 및 .mdx 파일은 제외됩니다. Markdown에서 줄 끝의 두 칸 공백은 하드 줄바꿈(<br>)을 의미하므로 제거하면 문서 형식이 깨집니다.
| 기준 | Write | Edit |
|---|---|---|
| 전송량 | 매번 전체 파일 전송 | 차이분만 전송 (단일 상수 변경에 약 20자) |
| 적합한 용도 | 새 파일 생성, 전체 재작성 | 부분 수정, 변수명 변경, 줄 추가/삭제 |
| 따옴표 정규화 | 미지원 | 지원 (findActualString + preserveQuoteStyle) |
| 다중 위치 교체 | 해당 없음 (전체 교체) | replace_all: true로 지원 |
선택 가이드: 기존 파일에서 소수의 줄만 변경할 때는 Edit이 토큰 효율적입니다. 파일의 대부분을 변경하거나 새 파일을 처음부터 작성할 때는 Write가 직관적입니다.
읽기-쓰기 계약은 4단계로 실행됩니다:
readFileState에 저장: 파일 경로, mtime, 내용 해시, 부분 읽기 여부(isPartialView)를 기록validateInput()이 검사: Write/Edit 호출 시 해당 파일에 대한 readFileState 존재 여부와 부분 읽기 여부를 확인call()이 원자적 낡음 검사 재실행: 실제 쓰기 직전에 파일의 현재 mtime을 readFileState와 비교하여 중간에 변경되지 않았는지 최종 확인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을 재시도해야 합니다.
findActualString + preserveQuoteStyle)는 직선/둥근 따옴표 불일치를 자동으로 처리합니다replace_all: true로 우회 가능합니다.md/.mdx 파일에서는 후행 공백을 유지합니다 — Markdown 하드 줄바꿈을 보존하기 위한 의도적 예외입니다Q1. offset/limit으로 파일의 일부만 Read한 후 Write하면 어떻게 되나요?
offset/limit 사용)는 isPartialView: true로 기록됩니다. Write의 validateInput()은 이를 확인하여 errorCode 2를 반환합니다. 파일 전체를 보지 않고 전체를 교체하는 것은 데이터 손실 위험이 있기 때문입니다.Q2. Edit이 "3개 일치 발견" 오류를 반환할 때 올바른 해결법은?
replace_all: true가 적합합니다. 특정 하나만 변경하려면 old_string에 주변 코드를 더 포함하여 고유하게 만들어야 합니다.Q3. 256KB Read 게이트를 초과하는 파일에 대해 "잘라내기 대신 오류"로 되돌린 이유는?
offset/limit으로 필요한 부분만 읽거나 다른 전략을 선택할 수 있습니다.Q4. 둥근 따옴표로 작성된 파일에 직선 따옴표로 old_string을 지정하여 Edit하면?
findActualString()은 파일과 검색 문자열을 모두 직선 따옴표로 정규화하여 위치를 찾습니다. 일치가 확인되면 preserveQuoteStyle()이 new_string에 원본의 둥근 따옴표 패턴을 적용합니다. 사용자가 따옴표 스타일을 몰라도 편집이 가능합니다.Q5. 중복 제거 시스템이 파일 미변경 시 반환하는 것은?
file_unchanged 타입의 결과와 약 100바이트의 짧은 스텁 메시지를 반환합니다. 전체 내용을 재전송하지 않으므로 컨텍스트 윈도우를 절약하면서도 모델에게 파일이 변경되지 않았음을 알립니다.