Lo que serás capaz de hacer
- Saber qué vuelve después de un borrado (trabajo con commit, archivos archivados) y qué no.
- Añadir un bloque de denegación a la configuración de Claude Code y saber qué formas de borrado se le escapan.
- Escribir un hook PreToolUse que bloquee los borrados y le diga al agente qué hacer en su lugar, y probarlo con JSON de ejemplo.
- Decidir qué capas protegen tus ejecuciones desatendidas, sabiendo que se saltan los avisos.
- Aplicar la misma forma de protección en OpenCode y Kimi Code CLI.
Ideas clave
- Para que Claude Code no borre tus archivos, bloquea los borrados con una regla de denegación y un hook PreToolUse, y haz commit y push para que un borrado que se escape tenga vuelta atrás.
- En nuestras pruebas con --dangerously-skip-permissions, una regla de denegación de Claude Code y un hook PreToolUse bloquearon rm cada uno por su cuenta; los avisos de aprobación nunca aparecen en las ejecuciones desatendidas.
- Las reglas de denegación comparan el texto del comando, así que find -delete, /bin/rm y un comando de una línea en Python se les escaparon; nuestro hook detectó los dos primeros y no el tercero, porque un hook solo bloquea lo que nombran sus patrones.
- Un mensaje de bloqueo que nombra la alternativa segura (mover los archivos a la carpeta _archive/ del proyecto) evita que el agente improvise para esquivar el bloqueo.
- Un hook que compara texto bloquea de más algunos comandos inofensivos; pruébalo con casos de prueba y acepta el falso positivo antes que un borrado que se escapa.
Si Claude Code ha borrado tus archivos, solo vuelve lo que existe en otro sitio. Para que Claude Code no vuelva a borrar archivos, sube tu trabajo al remoto y después añade una regla de denegación y un hook. Esta guía monta cada capa para Claude Code en unos 30 minutos y muestra qué se le escapa a cada una.
Usamos una protección contra borrados en cada sesión de Claude Code de nuestro propio repositorio. Los ejemplos usan Marrowby Tiles, una tienda de azulejos inventada; las reglas y los patrones son un reflejo de los nuestros.
Claude Code ha borrado archivos: qué se puede recuperar
La vía de vuelta se construye antes del borrado, nunca después.
| Se recupera | Se pierde para siempre |
|---|---|
| Archivos con commit (local o subido); solo los commits subidos sobreviven a una carpeta perdida o vaciada | Archivos sin commit eliminados con rm, find -delete o un script |
Archivos que un hook redirigió a _archive/ en lugar de borrarlos | Cambios sin commit que se pierden con git checkout --, git reset --hard o git restore |
¿Acabas de perder archivos? Ejecuta esto en orden:
- Mira en la carpeta
_archive/del proyecto. - Ejecuta
git statuspara ver qué archivos con seguimiento faltan. - ¿Tenía commit pero no lo habías subido?
git restore --source=HEAD --staged --worktree <path>lo recupera de tu último commit local. - Ejecuta
git fetchy despuésgit restore --source=origin/main <path>para recuperar un archivo tal como se subió por última vez (pon el nombre de tu rama). - Si un commit eliminó el archivo,
git log --diff-filter=D -- <path>te dice cuál fue.
Lo que no recuperen estos pasos ha desaparecido del disco.
Por qué Claude Code borra archivos
Un aviso de aprobación protege a quien está delante del teclado. No hace nada por una ejecución a las 3 de la madrugada.
Los agentes borran por motivos corrientes: si les pides «empezar de cero», Claude Code vacía primero la carpeta, y en nuestra prueba en una carpeta desechable eligió find -delete por su cuenta. Git también borra: git reset --hard, git clean, git restore, git checkout -- y git stash drop descartan el trabajo sin commit, y un simple git stash lo esconde hasta que haces pop.
El aviso de aprobación es la capa más débil. La gente aprueba lo que lee por encima, y nuestras ejecuciones programadas de agentes usan --dangerously-skip-permissions, así que no aparece ningún aviso.
| Métrica | Valor | Fuente |
|---|---|---|
| Comandos peligrosos detectados por revisores humanos en un estudio controlado | 13,6 % | Anthropic(se abre en una pestaña nueva), 2026 |
| Comandos peligrosos detectados por el clasificador del modo automático de Claude Code, en el mismo estudio | 89 % | Anthropic(se abre en una pestaña nueva), 2026 |
| Archivos que Claude Code habría borrado a un desarrollador | 48 000 en 103 segundos | TechRadar(se abre en una pestaña nueva), 2026 |
Mira cómo ConvOps pone un paso de aprobación antes de cada merge(se abre en una pestaña nueva), para que un borrado hecho durante una ejecución desatendida nunca llegue a main sin el sí de una persona.
Qué capa frena de verdad un borrado
La lista de denegación es un badén. El hook es un muro con huecos conocidos. El trabajo subido es el suelo.
¿Qué es una regla de denegación? Una regla de denegación es una línea de la configuración de Claude Code que rechaza cualquier comando Bash cuyo texto coincida con un patrón, como Bash(rm *).
¿Qué es un hook PreToolUse? Un hook PreToolUse es un script que Claude Code ejecuta antes de una llamada a una herramienta; cuando el script sale con el código 2, la llamada se bloquea y el agente lee el mensaje del script.

En nuestras pruebas, una regla de denegación y un hook bloquearon rm cada uno por su cuenta. Ninguno de los dos lo paró todo.

Haz commit y push antes de que un agente toque nada
El trabajo sin subir muere con la carpeta que lo contiene.
Antes de arrancar Claude Code, guarda todo en el remoto:
git status
git add src/ data/products.json
git commit -m "Save work before the agent run"
git push
Arranca el agente solo con el árbol de trabajo limpio y al día con el remoto. Nuestros flujos de trabajo suben el resultado de cada paso antes de avanzar, y un hook de parada impide cerrar una sesión con trabajo sin subir. Haz el trabajo arriesgado en un git worktree, como ~/marrowby-shop-run-0412, o en una ejecución aislada que parta del remoto.
Añade reglas de denegación a Claude Code
Las reglas de denegación comparan el texto del comando, no lo que hace el comando.
Este bloque va en el .claude/settings.json del proyecto:
{
"permissions": {
"deny": [
"Bash(rm *)",
"Bash(rmdir *)",
"Bash(git rm *)",
"Bash(git clean *)",
"Bash(git reset *)",
"Bash(git restore *)",
"Bash(git checkout -- *)",
"Bash(git stash drop *)",
"Bash(git stash clear *)",
"Bash(git push --force *)",
"Bash(git push -f *)"
]
}
}
Bash(rm *) y el antiguo Bash(rm:*) se comportaron igual en la 2.1.295, incluso con --dangerously-skip-permissions. La documentación de los modos de permisos(se abre en una pestaña nueva) de Anthropic dice que las reglas de denegación «bloquean en todos los modos, incluido bypassPermissions», y cuenta el modo automático entre esos modos. La documentación de permisos(se abre en una pestaña nueva) también advierte que una regla de denegación «no es una barrera de seguridad alrededor del programa». En nuestra ejecución, find -delete, /bin/rm y un comando de una línea con python3 -c borraron el archivo cada uno; sh -c 'rm ...' sí quedó bloqueado.
Añade un hook que bloquee y ofrezca una salida
Un bloqueo sin alternativa enseña al agente a esquivarte.
El .claude/settings.json completo, con una clave hooks junto a permissions (guía de hooks(se abre en una pestaña nueva)):
{
"permissions": {
"deny": [
"Bash(rm *)",
"Bash(rmdir *)",
"Bash(git rm *)",
"Bash(git clean *)",
"Bash(git reset *)",
"Bash(git restore *)",
"Bash(git checkout -- *)",
"Bash(git stash drop *)",
"Bash(git stash clear *)",
"Bash(git push --force *)",
"Bash(git push -f *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/delete-guard.py"],
"timeout": 5
}
]
}
]
}
}
${CLAUDE_PROJECT_DIR} apunta a la raíz del proyecto, así que la ruta sigue valiendo aunque el agente cambie de carpeta. Si la ruta del script está mal, python3 sale con el código 2 y cada llamada a Bash queda bloqueada con un error can't open file. Falla en modo cerrado, y el primer comando lo deja a la vista.
Y el script, .claude/hooks/delete-guard.py:
import json, re, sys
ARCHIVE = ("file deletion is denied, archive instead: move it to _archive/ "
"in this project (mkdir -p _archive && mv <path> _archive/)")
PATTERNS = [
(r"\brm\s+-rf\b", "rm -rf is irreversible recursive deletion, " + ARCHIVE),
(r"(?<!-)\brm\b", ARCHIVE),
(r"\brmdir\b", ARCHIVE),
(r"\bfind\b.*\s-delete\b", ARCHIVE),
(r"\bgit\s+checkout\s+--\s+", "git checkout -- reverts uncommitted work"),
(r"\bgit\s+reset\b", "git reset can destroy commit history"),
(r"\bgit\s+restore\b", "git restore reverts uncommitted work"),
(r"\bgit\s+clean\b", "git clean deletes untracked files"),
(r"\bgit\s+stash\b", "git stash can drop uncommitted work"),
(r"\bgit\s+push\b.*\s(?:-f|--force)\b", "force push overwrites remote history"),
]
MESSAGE_ARG = re.compile(r"""(?:^|\s)(?:-m|--message)(?:=|\s+)(?:"[^"]*"|'[^']*'|\S+)""")
def main():
try:
command = json.load(sys.stdin)["tool_input"]["command"]
except Exception:
return 0
scanned = MESSAGE_ARG.sub(" ", command)
for pattern, reason in PATTERNS:
if re.search(pattern, scanned):
print(f"BLOCKED: Destructive command detected.\n Reason: {reason}\n"
f" Command: {command}", file=sys.stderr)
return 2
return 0
sys.exit(main())
Cada línea tiene su motivo:
- El código de salida 2 bloquea la llamada; el agente lee stderr.
(?<!-)\brm\bdetecta cualquier tokenrm, incluido/bin/rm. La aserción hacia atrás (lookbehind) deja pasardocker run --rm.- Una entrada ilegible sale con 0: nunca atasca una sesión, pero queda sin revisar.
- Ningún patrón para intérpretes. Los comandos de una línea con
os.removeyshutil.rmtreepasan: el hook solo bloquea lo que nombran sus patrones.
Un hook se ejecuta en cada llamada a Bash, se le haya dicho lo que se le haya dicho al agente (ingeniería de grafos). Por eso una regla así va en un hook y no en un CLAUDE.md que no para de crecer.
Prueba el hook antes de fiarte de él
Una expresión regular sin probar es una suposición.
Pasa un JSON de ejemplo al script con una tubería:
echo '{"tool_name":"Bash","tool_input":{"command":"find exports -mindepth 1 -delete"}}' \
| python3 .claude/hooks/delete-guard.py; echo "exit $?"
Añade un caso de prueba por cada forma de borrado, incluidos los fallos que ya esperas:
| Comando | Resultado esperado |
|---|---|
rm -rf exports/ | sale con 2 |
git checkout -- src/ | sale con 2 |
docker run --rm alpine true | sale con 0 |
git commit -m "Never rm -rf exports, archive instead" | sale con 0 |
python3 -c "import os; os.remove('exports/product-feed.csv')" | sale con 0, un fallo conocido |
git push -f origin main | sale con 2 |
grep -rn "rm -rf" scripts/ | sale con 2, un falso positivo conocido |
Termina con un bloqueo real: en una carpeta de pruebas, pide a Claude Code que borre un archivo desechable.
Un intento de borrado, paso a paso
El mejor bloqueo es aquel del que el agente se recupera solo.

- Disparador. Marrowby Tiles pide: «Regenera la exportación del feed de productos, empieza de cero». El agente ejecuta
find exports -mindepth 1 -delete(marca 1). - Bloqueo. El hook coincide con
\bfind\b.*\s-delete\by sale con 2; su mensaje nombra_archive/en este proyecto (marca 2). - Redirección. El agente ejecuta
mkdir -p _archive && mv exports/* _archive/(marca 3). - Veredicto. 2 elementos movidos y 0 borrados:
_archive/product-feed.csvy_archive/old/(marca 4).
Qué falla en una protección contra borrados
Preferimos explicar un bloqueo de más que restaurar una carpeta.

Cuatro fallos que hemos sufrido, cada uno con su regla:
- Se bloqueó una búsqueda. Un
grep -rn "rm -rf" scripts/de solo lectura coincidió. Regla: mantén el falso positivo (fallo en modo cerrado) y su caso de prueba. - Los mensajes de commit disparaban el hook. «Never rm -rf exports» coincidía como si fuera un comando. Regla: quita primero los argumentos
-my--messageentre comillas. - La ruta de archivado salía del proyecto. Nuestro mensaje de bloqueo nombraba una carpeta en la raíz de nuestro repositorio, y un agente de prueba de otro proyecto movió allí sus archivos. Regla: nombra
_archive/en este proyecto. - Un comando de git borró trabajo.
git checkout --sobre dos carpetas eliminó trabajo sin commit. Regla: deniega y bloquea con el hook también los comandos de git que borran.
Cuando nadie está mirando
Los avisos no son un control para las ejecuciones desatendidas. Las aprobaciones y el aislamiento, sí.
Nuestras ejecuciones desatendidas omiten los avisos por diseño. Tres controles las sostienen:
| Control | Qué hace | Qué no hace |
|---|---|---|
| Reglas de denegación y el hook | Bloquean en ejecuciones locales las formas de borrado que nombran | Detectar las formas que no nombran |
| Ejecución aislada | Ejecuta cada trabajo en su propio entorno, como marrowby-run, a partir de commits subidos, así que un borrado no puede llegar a tu copia | Frenar un comando dentro de la ejecución |
| Paso de aprobación antes del merge | Una persona aprueba antes de que un cambio, borrado incluido, llegue a main | Frenar un comando durante la ejecución |
Nuestros flujos de trabajo se detienen para pedir aprobación allí donde está el riesgo (humano en el bucle), y nuestro bucle de reparación del sitio se ejecuta en un entorno aislado.
Copia hoy el bloque de denegación y el hook. Después, ejecuta tus trabajos desatendidos de agentes como tareas de ConvOps(se abre en una pestaña nueva), con un paso de aprobación antes del merge y una ejecución aislada.
La misma protección en OpenCode y Kimi Code CLI
Una misma forma de protección, tres archivos.
Probamos esta protección en proyectos de prueba; la que usamos a diario funciona en Claude Code.

OpenCode 1.18.31
En OpenCode, las reglas de denegación de permission.bash frenaron rm y find -delete, y entonces el agente borró con un comando de una línea en Python. Un plugin, .opencode/plugins/delete-guard.js, revisa cada comando bash en tool.execute.before, con os.remove y shutil.rmtree entre sus patrones, y lanza un mensaje que nombra _archive/. Bloqueó las seis formas que probamos, y el agente archivó en lugar de borrar. opencode run --pure omite los plugins, protección incluida.
Kimi Code CLI 0.31.1
Kimi Code CLI (ejecuta modelos como Kimi K3) admite un hook PreToolUse en ~/.kimi-code/config.toml:
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "node ~/.kimi-code/hooks/delete-guard.mjs"
El script sale con 2 con el mismo mensaje y, en modo yolo, bloqueó las seis formas. Los hooks solo viven en la configuración del usuario, nunca en el proyecto. La regla nativa Bash(rm *) nunca coincidió con una ruta que llevara barra; Bash(rm */**) sí.
Uses la herramienta que uses, el orden es el mismo: subir, denegar, añadir el hook y probar.
Pasos
Haz commit y push de tu trabajo
Ejecuta git status, haz commit de todo y súbelo antes de arrancar Claude Code. Haz el trabajo arriesgado en un git worktree o en una ejecución aislada que parta del remoto. Entregable: nada que te importe existe solo en tu disco.
Añade reglas de denegación a Claude Code
Pega un bloque permissions.deny en el .claude/settings.json del proyecto que cubra rm, rmdir, git rm, git clean, git reset, git restore, git checkout sobre una ruta, git stash drop y clear, y el push forzado. Entregable: Claude Code rechaza esos comandos, incluso con --dangerously-skip-permissions.
Añade un hook PreToolUse que indique la salida segura
Guarda delete-guard.py en .claude/hooks/ y conéctalo como hook PreToolUse con el matcher Bash. Entregable: un comando que coincide sale con 2 y el agente lee un mensaje que le dice que mueva los archivos a _archive/ en este proyecto.
Prueba el hook con JSON de ejemplo
Pasa al script un JSON de ejemplo por cada forma de borrado y comprueba el código de salida: 2 para los borrados y 0 para docker run --rm y para los mensajes de commit. Después provoca un bloqueo real en una carpeta de pruebas. Entregable: una lista de casos de prueba que también recoge el fallo conocido y el falso positivo conocido.
Aísla las ejecuciones desatendidas
Haz los trabajos programados de agentes en un entorno aislado que parta de commits subidos, con un paso de aprobación antes del merge. Entregable: un borrado durante una ejecución no llega ni a tu copia ni a main sin el sí de una persona. Crea una cuenta gratuita de ConvOps en https://my.convops.app/register para ejecutarlos como tareas.
Preguntas frecuentes
¿Qué impide que Claude Code borre archivos?
Hay cuatro capas que impiden que Claude Code borre archivos: un aviso de aprobación, una regla de denegación en .claude/settings.json, un hook PreToolUse que revisa cada llamada a Bash y una vía de vuelta hecha de trabajo con commit y subido, más ejecuciones aisladas. Solo las tres últimas funcionan en ejecuciones desatendidas, que se saltan los avisos. Una regla de denegación compara el texto del comando, así que find con -delete y /bin/rm se le escapan, y un hook solo bloquea los patrones que nombra.
¿Quién necesita impedir que Claude Code borre archivos?
Los desarrolladores y equipos que usan Claude Code en repositorios reales necesitan una protección contra borrados, sobre todo cuando los trabajos de agentes se ejecutan desatendidos o de forma programada. El resultado es que un borrado queda bloqueado o el trabajo sigue existiendo en el remoto. ConvOps ejecuta esos trabajos como tareas, con una ejecución aislada y un paso de aprobación antes del merge, así que un borrado hecho durante una ejecución no llega ni a tu copia local ni a main sin el sí de una persona.
¿Ha borrado Claude archivos?
Para saber si Claude Code ha borrado archivos, revisa las llamadas a la herramienta Bash de la sesión en busca de formas de borrado: rm, rmdir, find con -delete, git checkout sobre una ruta, git reset --hard, git clean o un comando de una línea con python3 -c. Después ejecuta git status para ver qué archivos con seguimiento faltan. Un archivo que haya eliminado un comando de Bash ha desaparecido del disco, salvo que tuviera commit (subido o local) o que un hook lo moviera a una carpeta _archive/ en lugar de borrarlo.
¿Cómo evitar que Claude borre archivos?
Haz commit y push de tu trabajo antes de arrancar Claude Code. Después añade a .claude/settings.json reglas de denegación para rm, rmdir y los comandos destructivos de git, añade un hook PreToolUse que salga con el código 2 y le diga al agente que mueva los archivos a _archive/, y prueba el hook pasándole JSON de ejemplo. Por último, haz los trabajos desatendidos en ejecuciones aisladas. Las reglas de denegación solo comparan el texto del comando, así que el hook y el trabajo subido cubren lo que se les escapa.
¿Por qué Claude ha borrado mis proyectos?
Claude Code borra archivos cuando una tarea suena a reinicio: si le pides empezar de cero, primero vacía la carpeta, y en nuestra prueba eligió find con -delete por su cuenta. Los comandos de git que revierten trabajo, como git checkout sobre una ruta, git reset --hard o git clean, también borran los cambios sin commit. Sin una regla de denegación ni un hook, nada se interpone entre el comando y el disco.
¿Puede Claude recuperar archivos borrados?
Claude Code solo recupera lo que existe fuera de los archivos de trabajo: los commits, locales o subidos (solo los subidos sobreviven a una carpeta perdida o vaciada), y los archivos que un hook redirigió a una carpeta _archive/ en lugar de borrarlos. El trabajo sin commit que se pierde con un borrado de Bash o una reversión de git no vuelve. Las ejecuciones aisladas de ConvOps parten de commits subidos en su propio entorno, así que un borrado dentro de una de ellas no puede llegar a tu copia local.
¿Siguen funcionando las reglas de denegación en el modo bypassPermissions?
Sí. En nuestras pruebas con Claude Code 2.1.295 y --dangerously-skip-permissions, la regla de denegación Bash(rm *) bloqueó rm, y un hook PreToolUse que sale con el código 2 también lo bloqueó, tal como indica la documentación de modos de permisos de Anthropic. La limitación: la misma regla de denegación dejó que find con -delete, /bin/rm y un comando de una línea en Python borraran el archivo, porque las reglas de denegación comparan el texto del comando, no lo que hace.
