Skip to content

Safe Editing Protocol (PROTOCOLO BLINDADO)

Safe Editing Protocol — PROTOCOLO BLINDADO (v4.5.x)

Section titled “Safe Editing Protocol — PROTOCOLO BLINDADO (v4.5.x)”

For: Claude Code, Claude Desktop, and developers Version: 4.5.29 | Last updated: 2026-07-11

This is the v4.5.x evolution of the original PROTOCOLO BLINDADO from v3.x. The eight rules below incorporate the rewrite guard (v4.5.10), post-edit hash chaining (v4.5.15), structured edit responses (v4.5.15), automatic OCC (v4.5.17), and step-through undo recovery (v4.3.8, still the recommended recovery path).

If you remember only one thing: REGLA 0 first, REGLA 5 last.


edit_file in mode replace does exact text substitution. It does not rewrite a file — it swaps the matched block and leaves the rest intact. A documented 2026-06-11 incident: old_text was a 15-line header and new_text was the full 150-line file body. The edit returned success, but the file had 298 lines with the procedure duplicated at the bottom — the model intended a rewrite, the tool did a partial replace, and the bug was silent.

v4.5.10 added a server-side guard that blocks this anti-pattern. The guard fires when all three signals match:

  1. len(new_text) > 2 × len(old_text) (size ratio)
  2. The file has more than 50% of its content outside the matched block
  3. len(new_text) > 500 bytes and > 50% of file size

When it fires, the edit is rejected with:

“looks like an accidental full-file rewrite; use write_file instead. Pass allow_rewrite:true to override (creates a safety backup).”

Use write_file for whole-file rewrites. Override only when you are sure the edit is correct (allow_rewrite:true creates a safety backup).

The force flag does not bypass this guard (decoupled in v4.5.14). Use allow_rewrite for that.


Before any edit, verify the exact content you intend to change:

// 1. Read the file (range read = token-efficient)
read_file({path: "file.cs", start_line: 10, end_line: 30})
// 2. Confirm the text exists (count check)
search_files({path: "file.cs", pattern: "exact text from read", count_only: true})
// → must return > 0
// 3. (Optional) Dry-run the edit / dry-run an analyze
analyze_operation({path: "file.cs", operation: "edit", old_text: "...", new_text: "..."})

search_files without include_content:true is a filename search (v4.5.24 fix). If you want to grep file contents, pass include_content:true or use any of output_format:json / output:content / context_lines:N (all auto-flip include_content).


REGLA 2 — Capture literal text (never retype)

Section titled “REGLA 2 — Capture literal text (never retype)”

Copy the old_text exactly from the read result. Do not retype it from memory. In particular watch out for:

  • Tabs vs spaces (model sees spaces, file has tabs → no match)
  • CRLF vs LF (Windows-edited files may have \r\n)
  • Trailing whitespace (model strips it, file has it → no match)

If your old_text keeps not matching, try tolerant_whitespace:true (v4.5.7+). It treats one tab as 4 spaces and CRLF/CR as LF, while preserving the file’s original bytes verbatim outside the match region. Conservative — does not normalize runs of multiple spaces.

// Mixed tabs/spaces in the file? Try tolerant matching:
edit_file({
path: "file.js",
old_text: " taula_llistat(",
new_text: " taula_llistat_new(",
tolerant_whitespace: true
})

REGLA 3 — Same file, multiple changes → multi_edit

Section titled “REGLA 3 — Same file, multiple changes → multi_edit”

A successful edit_file invalidates the file cache and updates the session’s OCC baseline (so a follow-up edit_file immediately works — no re-read needed). But it still does N round-trips and N backups.

For 2+ edits in the same file, prefer multi_edit:

multi_edit({
path: "file.go",
edits_json: "[
{\"old_text\":\"foo()\", \"new_text\":\"bar()\"},
{\"old_text\":\"baz()\", \"new_text\":\"qux()\"}
]",
expected_hash: "1a2b3c4d" // optional — same OCC semantics as edit_file
})

For edits across many files, prefer batch_operations({request_json}) with atomic:true. For complex multi-step refactors, batch_operations({pipeline_json: "..."}) with parallel:true runs independent steps concurrently via the DAG scheduler.


REGLA 4 — OCC via expected_hash (v4.5.6+) and --auto-occ (v4.5.17+)

Section titled “REGLA 4 — OCC via expected_hash (v4.5.6+) and --auto-occ (v4.5.17+)”

Two complementary mechanisms protect against stale reads:

// First edit: read returns content_hash (now in StructuredContent)
read_file({path: "file.go"})
// → structuredContent.content_hash: "1a2b3c4d"
// → text body (no trailer)
// Second edit: pass the previous content_hash as expected_hash
edit_file({
path: "file.go",
old_text: "foo()",
new_text: "bar()",
expected_hash: "1a2b3c4d"
})
// On mismatch:
// "stale edit: file content changed since read
// (expected hash: 1a2b3c4d, actual: 9z8y7x6w).
// Re-read the file with read_file to get the current content_hash, then retry."

multi_edit also accepts expected_hash (v4.5.13+). The check happens once before the atomic loop, so a stale hash never creates a backup and never applies any edits.

The session remembers the content_hash of every file it last read or last wrote. Before an edit without explicit expected_hash, the engine compares on-disk hash to that baseline. If something else modified the file, it fires.

Modes (set once at startup):

ModeBehavior when on-disk hash differs from session baseline
offNo check
warnAppend a non-blocking notice to the edit response
blockReject the edit with a clear error

warn is the default. Critical correctness property: the baseline is updated on the session’s own writes (using the post-edit content_hash from v4.5.15), so consecutive edits never false-positive.

v4.5.26 — write_file now refreshes the OCC baseline too. Until v4.5.25, write_file did not call RecordWriteHash, so a write → edit chain in the same session would trip a false external_change warning on the very next edit (the write was recorded as a “read of the prior content” but not as a write of the new content). write_file now records its post-write content_hash immediately, so chaining write_file(path, content)edit_file(path, old_text, new_text, expected_hash: <hash from write>) works without spurious OCC notices. See Structured Output for the new typed content_hash field on the write_file response.


Pick the right edit_file mode (or a different tool) for the situation:

SituationUse
One exact text replacement (single occurrence)edit_file (default mode:"replace")
One exact text, but file has it N timesedit_file({old_text, new_text, occurrence: N}) (1=first, -1=last)
Global literal find/replaceedit_file({mode:"search_replace", pattern, replacement})
Regex with capture groupsedit_file({mode:"regex", patterns_json})
Delete a known line range (e.g. lines 45-60)edit_file({mode:"delete_range", start_line, end_line})
Replace a known line rangeedit_file({mode:"replace_range", start_line, end_line, new_text})
Whole-file rewrite (small file, < 5 KB)write_file (NOT edit_file)
Multi-edit same filemulti_edit
Project-wide find/replaceproject_replace
Cross-file atomic editsbatch_operations({request_json})
Multi-step pipeline with DAG parallelismbatch_operations({pipeline_json})
Delete a single filedelete_file (soft — recoverable via SD-ID)
Cross-file delete with confirmationanalyze_operation({operation:"delete"}) first

Pre-flight check before any global replace: search_files({pattern, count_only:true}) to verify the impact.

diff_format (v4.5.14+) controls the diff verbosity: "" / "auto" (default: full when small, summary past 200 diff lines), "full", "summary" (per-hunk ranges + 3 anchor lines), "stat" (+N -M), "none". The same parameter works on multi_edit (v4.5.25 — aggregate diff of the whole batch).


REGLA 6 — Dry-run before destructive ops

Section titled “REGLA 6 — Dry-run before destructive ops”
OpDry-run call
Whole-file deleteanalyze_operation({path, operation:"delete"})
Whole-file writeanalyze_operation({path, operation:"write"})
Single editedit_file({..., dry_run:true}) (regex / search_replace)
Pipeline executionbatch_operations({pipeline_json: "...", dry_run:true})
Project-wide replaceproject_replace({path, find, replace, preview:true})
WSL syncwsl({action:"sync", direction:"...", dry_run:true})
Minify JSminify_js({path, dry_run:true})

analyze_operation returns risk level (LOW/MEDIUM/HIGH/CRITICAL), change percentage, and impact preview — no file is touched.


REGLA 7 — Backup recovery (step-through undo)

Section titled “REGLA 7 — Backup recovery (step-through undo)”

Every successful edit creates a backup linked into a per-file chain via PreviousBackupID. Compact responses show a truncated ID for display:

M file.go | 7@+7-0 | 42L | UNDO:20260501-123650 | chain:20260501-123649

The chain is your rollback path. Step-through is preferred over a full restore because it lets you undo one edit at a time:

// Undo the most recent edit on a file
backup({action: "undo_last", file_path: "file.go"})
// Preview what would be undone (no write)
backup({action: "undo_last", file_path: "file.go", preview: true})
// Walk further back the chain
backup({action: "undo_last", file_path: "file.go"}) // call again
// See the full chain
backup({action: "undo_chain", file_path: "file.go"})
// Restore a specific backup by full ID
backup({
action: "restore",
backup_id: "20260501-123650-333c964cc3af7a82", // full ID, not truncated
file_path: "file.go"
})
// Find backups for a specific file
backup({action: "list", filter_path: "file.go"})

Soft-delete recovery (v4.5.11+) — every delete_file (soft, default) returns a Soft-Delete ID (SD-ID). Restore via the same backup tool:

// Discover
backup({action: "list_trash", filter_path: "file.go"})
// Restore
backup({action: "restore_trash", sd_id: "sd-20260611-150455-a1b2c3d4"})
// Cleanup old
backup({action: "purge_trash", older_than_days: 7, dry_run: true})

HIGH and CRITICAL edits auto-verify file integrity after writing (readable? reasonable size? line count? CRC32 hash). If the file is truncated or corrupted, the response reports it immediately.


## Before editing file: <FILENAME>
### STEP 1 — Read & verify
- [ ] read_file({path, start_line, end_line}) and copy EXACT bytes
- [ ] Note line numbers
- [ ] (Optional) analyze_operation({operation:"edit", ...}) for risk
### STEP 2 — Confirm pattern
- [ ] search_files({pattern, count_only:true})
- [ ] Expected vs actual count match
### STEP 3 — Choose mode
- [ ] Whole file → write_file (or REGLA 0 will block)
- [ ] Single surgical change → edit_file (default mode)
- [ ] Multiple changes → multi_edit
- [ ] Line range delete → edit_file({mode:"delete_range", start_line, end_line})
- [ ] Cross-file atomic → batch_operations({request_json, atomic:true})
### STEP 4 — OCC + safety
- [ ] Pass expected_hash from previous read/edit (or rely on --auto-occ=warn)
- [ ] High/critical risk: pass force:true; review VERIFY instruction in response
### STEP 5 — Execute
- [ ] edit_file / multi_edit / write_file / batch_operations call
- [ ] Capture the UNDO: ID and chain: parent ID from the response
### STEP 6 — Verify
- [ ] search_files({pattern: old_text, count_only:true}) → 0 matches
- [ ] read_file({path, start_line, end_line}) to confirm result

  • File changed since your read → re-read it
  • Whitespace mismatch → try tolerant_whitespace:true
  • Multi-occurrence but you expected one → either provide enough context to make it unique, or pass occurrence: N

“stale edit: file content changed since read”

Section titled ““stale edit: file content changed since read””
  • OCC detected a hash mismatch. Re-read and use the new StructuredContent["content_hash"] as your expected_hash.
  • Or relax to warn mode via --auto-occ=warn (default) — it appends a notice instead of blocking.

“looks like an accidental full-file rewrite”

Section titled ““looks like an accidental full-file rewrite””
  • REGLA 0 fired. Use write_file for whole-file rewrites.
  • If the rewrite is genuinely intended, pass allow_rewrite:true.

“OPERATION BLOCKED — HIGH/CRITICAL risk”

Section titled ““OPERATION BLOCKED — HIGH/CRITICAL risk””
  • v3.x behaviour. In v4.5.x, HIGH/CRITICAL proceed with a backup and a VERIFY instruction. force:true is no longer required for them; it is reserved for the risk-threshold bypass.

“Destructive git operation ‘restore’ requires force=true”

Section titled ““Destructive git operation ‘restore’ requires force=true””
  • For git(action:"restore", ...) against the working tree (no staged:true, no dry_run:true), the engine requires force:true. For unstage (staged:true) and preview (dry_run:true), force is not needed.

If anything goes wrong, the recovery order is:

  1. Stop editing. More edits can mask the original state.
  2. List the chain: backup({action:"undo_chain", file_path:"file.go"})
  3. Preview the undo: backup({action:"undo_last", file_path:"file.go", preview:true})
  4. Step back: backup({action:"undo_last", file_path:"file.go"})
  5. For soft-deletes: backup({action:"list_trash", filter_path:"file.go"}) then backup({action:"restore_trash", sd_id:"..."})
  6. Full recovery guide at runtime: server_info({action:"help", topic:"recovery"}) or help({topic:"recovery"}).


Version: v4.5.29 | Last updated: 2026-07-11