Three Tools, One Contract
Claude Code prove tres primitivas de manipulacao de arquivo compartilhando um invariante: toda operacao de escrita requer uma leitura previa do arquivo-alvo.
| Capability | Read | Write | Edit |
|---|---|---|---|
| Read-only / concurrency-safe | Yes | No | No |
| Requires prior Read | -- | Yes | Yes |
| Handles images natively | Yes | No | No |
| Handles PDFs | If supported | No | No |
| Quote normalization | -- | -- | Yes |
| Dedup (skip unchanged) | Yes | -- | -- |
| Token limit enforced | 25K default | -- | -- |
| LSP notifications | No | Yes | Yes |
Read Tool Deep-Dive
Pagination: offset + limit
Por default Read retorna ate 2000 linhas a partir da linha 1. Para arquivos grandes, use offset (1-based) e limit.
Token Limit Enforcement
Gate de dois estagios: primeiro uma estimativa rapida (sem API call). Se exceder 1/4 do cap, contagem exata via API. Se exceder maxTokens (default 25K), MaxFileReadTokenExceededError e thrown antes do conteudo ser enviado.
Uma abordagem de truncation foi testada e revertida. Truncation produzia ~25K tokens enquanto o throw produz ~100 bytes de erro, reduzindo dramaticamente contexto desperdicado.
Dedup: Como Read Evita Re-envio
readFileState.set(fullFilePath, {
content,
timestamp: getFileModificationTime(fullFilePath),
offset, // undefined means "full read"
limit,
})
No proximo Read do mesmo arquivo e range, a tool checa mtime no disco. Se igual, retorna stub leve em vez de re-enviar conteudo.
Duas copias completas por turno desperdicam cache_creation tokens em cada turno subsequente. O dedup path produz ~100-byte stub vs ate 25K tokens de conteudo. GrowthBook killswitch tengu_read_dedup_killswitch pode desabilitar.
Image, PDF e Notebook Support
- Images: PNG, JPG, JPEG, GIF, WebP -- resize via sharp, retornados como base64 block multimodal
- PDFs: pages parameter aceita ranges como "1-5". Hard cap de 20 pages por call
- Notebooks: .ipynb via mapNotebookCellsToToolResult() -- modelo ve visao unificada
- macOS: Screenshot filenames com thin space (U+202F) detectados e retried automaticamente
Write Tool Deep-Dive
Write e full-content replacement. Cria novo arquivo ou sobrescreve existente inteiramente.
Read-Before-Write Gate
- errorCode 2: No read in session -- "File has not been read yet."
- errorCode 2: Read was partial (offset/limit) -- recusa para prevenir overwrite de conteudo nao visto
- errorCode 3: File modified after read -- "Read it again before attempting to write."
Atomic Write Sequence
// 1. mkdir (async, before critical section)
await fs.mkdir(dir)
// 2. Backup for file history (async, keyed on content hash)
await fileHistoryTrackEdit(...)
// 3. Sync read + staleness check (critical section starts)
meta = readFileSyncWithMetadata(fullFilePath)
if (lastWriteTime > lastRead.timestamp) throw FILE_UNEXPECTEDLY_MODIFIED_ERROR
// 4. Write to disk (critical section ends)
writeTextContent(fullFilePath, content, enc, 'LF')
Write sempre persiste com LF line endings, independente do arquivo antigo. Uma abordagem anterior que preservava/inferia line endings corrompeu silenciosamente bash scripts quando sobrescrevia CRLF em Linux.
Edit Tool e Quote Normalization
Edit faz exact string replacement: encontra old_string e substitui por new_string. So envia o diff -- muito mais barato que Write para mudancas pequenas.
Quote Normalization
Claude nao pode outputtar curly (typographic) quotes -- a API sanitiza. O Edit tool resolve com processo de dois passos:
// normalizeQuotes() under the hood:
str
.replaceAll('\u2018', "'") // left single curly
.replaceAll('\u2019', "'") // right single curly
.replaceAll('\u201C', '"') // left double curly
.replaceAll('\u201D', '"') // right double curly
findActualString() normaliza ambos para straight quotes, localiza o match, e retorna a versao original curly-quote do arquivo. preserveQuoteStyle() aplica o mesmo estilo curly-quote ao new_string.
preserveQuoteStyle() nao converte cegamente todo ' para curly quote. Quando single quote esta entre duas letras Unicode (don't, it's), e tratado como apostrofo e recebe right single curly quote. Usa /\p{L}/u para deteccao.
Desanitization Table
'<fnr>' -> '<function_results>'
'<n>' -> '<name>'
'<o>' -> '<output>'
'<e>' -> '<error>'
'\n\nH:' -> '\n\nHuman:'
'\n\nA:' -> '\n\nAssistant:'
normalizeFileEditInput() strip trailing whitespace de cada linha do new_string. Excecao: .md e .mdx sao skipped -- Markdown usa dois trailing spaces como hard line break.
Write vs Edit: Side-by-Side
Write({
file_path: "/src/config.ts",
content: `import { z } from 'zod'
export const config = {
maxRetries: 5, // changed
timeout: 3000,
endpoint: "https://...",
}`
})
// Sends ENTIRE file. 150+ lines of tokens.
Edit({
file_path: "/src/config.ts",
old_string: "maxRetries: 3,",
new_string: "maxRetries: 5,",
})
// Sends ~20 characters.
// Costs a fraction of Write.
- Criando um novo arquivo do zero
- Substituindo conteudo com versao estruturalmente diferente (rewrite completo)
- Mudancas tao extensas que construir unique
old_stringrequereria a maior parte do arquivo
The Read-Before-Write Contract
4 Fases do Contrato
Um linter ou formatter rodando no file save pode modificar o arquivo entre Read e Write do Claude. Isso dispara o erro "file has been modified since read". Fix: re-read o arquivo apos formatting completar, ou configure o linter para nao auto-save durante a sessao do Claude.
Limits Precedence e Blocked Paths
- Env var
CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS-- user-set override, beats everything - GrowthBook flag
tengu_amber_wren-- per-org experiment infrastructure - Hardcoded default -- 25,000 tokens / 256 KB
Blocked Device Paths
// Infinite output -- never reach EOF
'/dev/zero', '/dev/random', '/dev/urandom', '/dev/full'
// Blocks waiting for input
'/dev/stdin', '/dev/tty', '/dev/console'
// fd aliases for stdio
'/dev/fd/0', '/dev/fd/1', '/dev/fd/2'
// Safe: /dev/null intentionally allowed
🎯 Resumo e Takeaways
Read e a unica tool read-only e concurrency-safe. Write e Edit requerem Read previa completa.
Dedup ~18% hit rate evita re-envio retornando ~100-byte stub quando mtime bate com timestamp cached.
Token cap 25K usa estimativa rapida primeiro; so faz API call para contagem exata se suspeito.
Edit's quote normalization lida com sanitizacao de curly quotes pela API. findActualString() busca com straight quotes mas escreve de volta curly.
Double staleness check em validateInput (pre-permission) e em call() (atomic, sync) fecha a race window.
Write truncation foi testada e revertida: throw no gate de 256KB e mais barato que enviar 25K tokens inutilizaveis.