# Guarding Destructive Operations > Guarda operaciones destructivas: borrados, sobrescrituras, reescritura de historial o resolución de nombres a rutas; rechaza en vez de avisar, comprueba antes de mutar, clasifica por estructura y prueba cada mitad. Fuente: https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/guarding-destructive-operations Markdown: https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/guarding-destructive-operations.md Repositorio: https://github.com/kajisho5/ffmpeg-skill Autor: kajisho5 Licencia: MIT Actualizado: hace 5 días Coste de contexto: 162 tok instalada, 2.6k tok al activarse, 2.6k tok con todos los archivos del bundle Bundle: 1 archivo, 10 KB Permisos que pide: ninguno declarado ## Instalación Un skill son archivos markdown: los mismos archivos valen para cualquier agente y lo único que cambia es el directorio de destino, es decir la bandera `--agent`. Añade `-g` para instalarlo en todos los proyectos de la máquina. ```bash # Claude Code npx -y skills add kajisho5/ffmpeg-skill --skill destructive-operations --agent claude-code # Cursor npx -y skills add kajisho5/ffmpeg-skill --skill destructive-operations --agent cursor # Codex npx -y skills add kajisho5/ffmpeg-skill --skill destructive-operations --agent codex # Gemini CLI npx -y skills add kajisho5/ffmpeg-skill --skill destructive-operations --agent gemini # Windsurf npx -y skills add kajisho5/ffmpeg-skill --skill destructive-operations --agent windsurf # Cline npx -y skills add kajisho5/ffmpeg-skill --skill destructive-operations --agent cline ``` ## Qué hace - Añade y revisa precondiciones en operaciones destructivas y hace que fallen antes de mutar nada si no se cumplen. - Rechaza con un error, sin avisos ni flags `--force`, y coloca la comprobación antes de la primera mutación. - Clasifica rutas por estructura (no por prefijo) y valida nombres además de comprobar que la ruta resuelta no escape del directorio base. - Hace explícito el desfase entre el `--dry-run` y la ejecución real, y comprueba que el rechazo llegue a quien llama. - Exige pruebas que muten cada mitad de la guarda por separado y que verifiquen que el destino queda intacto al rechazar. ## Cuándo usarla - Al escribir o revisar comandos `--rebuild`, `--reset` o `--purge`, borrados masivos o reescrituras de historial. - Al revisar un paso con forma de `rm -rf` o cualquier código que sobrescriba artefactos guardados. - Al revisar código que resuelve un nombre proporcionado por quien llama a una ruta del sistema de archivos. - Al auditar operaciones irreversibles que necesitan precondiciones claras y pruebas de que no mutan al fallar. ## Qué la activa - "Revísame este comando `--rebuild` y asegúrate de que no borre nada que no deba." - "Añade una guarda que haga fallar el `rm -rf` si el repositorio contiene algo fuera de lo permitido." - "Quiero validar que un nombre recibido como argumento no pueda escaparse del directorio base." - "Ayúdame a escribir una prueba de que un borrado rechazado no muta ningún archivo." ## Archivos - SKILL.md — 10 KB ## SKILL.md Reproducido tal cual desde kajisho5/ffmpeg-skill bajo MIT. Esta sección es el documento original y está en inglés. # Guarding Destructive Operations Some operations have no undo: an orphan-branch rewrite, a recursive delete of every tracked file, an overwrite of a stored artifact, a `DROP`/`purge` path. The implementation is usually short and usually correct *for the setup its author had in mind*. The bug is never the deletion itself — it is that nothing checked whether the target was the thing the author imagined. Two shapes recur, and they take the same fix: - **Blast radius.** The operation is correct only in a dedicated, single-purpose target and is catastrophic in a mixed one. - **Boundary escape.** A caller-supplied name is joined onto a base directory and lands outside it. ## Refuse; don't warn, and don't add an override A destructive operation that discovers its precondition is violated should return an error and change nothing. A warning is read by nobody, and a `--force` escape hatch turns the guard into documentation. ```go // A rebuild that recreates history does `checkout --orphan` then // `rm -rf --ignore-unmatch .`, keeping only the state files it rewrites. // That is correct in a dedicated mirror repository whose ONLY tracked content // is `.state/`. Run it where source code also lives and the new branch loses // the source code. func (e *Engine) rebuild() error { if err := e.ensureDedicatedRepo(); err != nil { return err // nothing mutated yet } ... } ``` The honest justification for having no override flag: there is no situation where the operator wants this command to delete unrelated tracked files. If a real one appears later, that is a separate, differently-named command — not a boolean on this one. ## Put the guard ahead of the first mutation "Refuses" is only true if the refusal happens before anything has changed. Walk the function and find the *first* statement with an effect — it is often not the obvious deletion. In-memory state clearing, a branch checkout, and a read-then-rewrite of the very files you are preserving all count. ```go // Order that makes the refusal safe: // 1. reject detached HEAD / ambiguous state // 2. ensureDedicatedRepo() <-- the new guard, nothing mutated yet // 3. read the state files to preserve // 4. CheckoutOrphan() // 5. RemoveAllTrackedFiles() // 6. ClearAllProgressCounts() ``` Steps 3-6 are all mutations of something (3 is not, but it is where the "what to preserve" snapshot is taken, and a guard after it has already committed you to a shape). Land the guard at 2 and a rejected run leaves the working tree byte-identical. Also refuse *ambiguous* state before rewriting it. A history rewrite that assumes a named branch should reject detached HEAD up front rather than silently recreating the wrong ref. ## Classify structurally, not by string prefix The guard needs to answer "is every tracked path inside the directory this command owns?". A `HasPrefix`/`startswith` on the bare directory name is the wrong answer and looks right in every test you would think to write. ```go // Bad — admits `.state-backup/notes.md` and `.stateful.txt`, so a repository // full of unrelated content passes the guard and gets deleted. func owned(path string) bool { return strings.HasPrefix(path, ".state") } // Good — the directory itself, or something genuinely under it. func owned(path string) bool { return path == ".state" || strings.HasPrefix(path, ".state/") } ``` The same distinction in Python is `Path(p) == base or base in Path(p).parents`, not `p.startswith(str(base))`. Any time a check compares paths as strings, ask what a sibling with a longer name does to it. ## Name validation and resolved containment are two checks, not one When a public argument selects a file — a baseline name, a template id, a report slug — do both, in this order: ```python NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") def _validate_name(name: str) -> str: if ".." in name or not NAME_RE.match(name): raise ValueError(f"invalid name: {name!r}") return name def _resolve(name: str, base: Path) -> Path: candidate = (base / f"{_validate_name(name)}.json").resolve() if not candidate.is_relative_to(base.resolve()): raise ValueError(f"name escapes {base}: {name!r}") return candidate ``` The containment half is **not** redundant with the regex. The regex stops `..` segments and absolute paths; only the `resolve()` + containment check stops a *symlink placed inside the directory* that points out of it — a name matching `^[A-Za-z0-9]` all the way through can still resolve anywhere. Route every entry point through both: the loader, the saver, and any path-building helper a public method still calls directly. A single unrouted helper reinstates the whole hole. Without this, an implicit suffix (`name + ".json"`) plus an absolute-path argument reads any file on disk whose name happens to end in that suffix. ## A guard on the destructive path is invisible to the preview If the real run reaches the guard but `--dry-run` short-circuits earlier, the preview cheerfully describes an operation the real run will refuse. That is a defensible design — the destructive path is the one that must be safe — but be explicit about it: - The preview will show a full rebuild in a repository where the rebuild is forbidden. Only the real invocation errors. - Anything meant to warn the operator *earlier* has to live in the preview stage or the CLI argument-validation layer, duplicated deliberately, not moved. Say which one you chose in the PR description; a reviewer cannot tell from the diff whether the dry-run gap is intentional. ## Know what the caller does with your refusal Tightening validation only helps if the new error reaches somebody. A broad `except Exception` two frames up will turn a hard refusal into an ordinary result shape: ```python try: features = self._load(baseline) except Exception as e: # already there, catches your ValueError return {"error": f"Analysis failed: {e}", "features": {}, "flags": {...}} ``` This is often *fine* — an API boundary that never raises is a real contract, and the refusal surfaces as a normal error-shaped response rather than a protocol error. But check it deliberately: if that shape is what your caller sees, the integration test asserts on `result["error"]`, not on `pytest.raises`. ## Testing - **Mutate each half separately.** Neuter only the regex and confirm a `..` fixture fails; neuter only the containment check and confirm a *symlink* fixture fails. One combined test passes with either half deleted and proves neither. - **Write the sibling-name fixture explicitly.** A test whose repository tracks `.state/data.json` passes against `HasPrefix(path, ".state")` too. The test that separates the implementations tracks `.state-backup/old.json` and asserts the operation is refused. - **Assert nothing was mutated on refusal**, not just that an error came back: the branch is unchanged, the tracked file list is unchanged, the in-memory progress counters are unchanged. That is the actual claim. - **Existing fixtures become the rejected shape.** Older tests for the destructive path were often built loosely — a repository tracking `app.txt` alongside the state directory, because nothing cared. After the guard, such a test no longer reaches the code it was written to cover; it now exercises the refusal. Tighten those fixtures to the allowed shape and add the refusal case as a *new* test, or you silently lose coverage of the destructive path. - **Reproduce the failure with the narrowest hook.** When you need one specific operation to fail in order to test the recovery path, target it precisely — a blanket hook that rejects every operation of that kind aborts the run earlier than the path you were trying to reach, and the test proves something else. ## Checklist - [ ] Every irreversible operation states its precondition in code, not in a doc - [ ] Violation returns an error; no `--force`, no warn-and-continue - [ ] The guard runs before the first statement with an effect - [ ] Ambiguous state (detached HEAD, missing target, multiple candidates) rejected up front - [ ] Path/ownership classification is structural, never a bare string prefix - [ ] Caller-supplied names are validated *and* resolved-path contained - [ ] Every entry point routes through both checks, including internal helpers - [ ] The dry-run/preview gap is intentional and stated - [ ] Tests mutate each half of the guard independently - [ ] A sibling-name (`x-backup/`) and a symlink fixture both exist - [ ] Refusal tests assert the target is unchanged, not just that an error was raised ## Note for this repository (ffmpeg-skill) This repo's actual exposure is much narrower than the examples above — there is no `--rebuild`/`--purge`-shaped command, no history rewriting, and "Keep originals" is already a stated design principle (`README.md` design principle #9: no tool overwrites its input; a test hashes every input after every run, see `tests/test_all.py`/`tests/test_contract.py`'s input-hash checks). The closest analogue is `-o`/output-path handling: every tool takes a caller-supplied output path and `ffmpeg_base()` always passes `-y` (overwrite without prompting) — there is no confirmation step before an existing file at that path is silently replaced. Since this is a local CLI a human or agent invokes directly (not a server resolving untrusted network input against a base directory), the "boundary escape" half of this skill is low-stakes here; the "refuse before the first mutation" and "existing input-hash tests still prove nothing was touched" halves are the parts that actually apply. Source: [wdm0006/python-skills](https://github.com/wdm0006/python-skills) (MIT). ## Dónde encaja - Categoría: [Herramientas para desarrolladores](https://skillsagentes.com/categorias/herramientas-desarrollo.md) — Skills que cambian cómo tu agente escribe, revisa y despliega código. - Creador: [kajisho5](https://skillsagentes.com/creators/kajisho5.md) — 0 skills en el directorio - [Todas las skills](https://skillsagentes.com/skills.md) - [Ranking de instalaciones](https://skillsagentes.com/ranking.md) ## Otras skills del mismo repositorio - [Ffmpeg Skill](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/ffmpeg-skill.md): Edita vídeo y audio con FFmpeg local desde lenguaje natural: cortes, unión, reframe 9:16/1:1, velocidad, subtítulos, overlays, multicámara/sync, LUFS, HDR→SDR, LUTs, export y verificación. Python 3.9 stdlib, sin nube ni API keys. - [Concurrent Branches](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/concurrent-branches.md): Resuelve conflictos de merge con varias ramas abiertas: unión, recomputación y reconstrucción como resoluciones correctas; artefactos generados, serialización no determinista e IDs renumerados; un auto-merge limpio no es un test que pasa. - [Ci Pipeline Synthesizer](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/ci-pipeline-synthesizer.md): Genera configuraciones de CI/CD para GitHub Actions: compilación y pruebas de librerías y paquetes. Úsalo al crear o actualizar workflows de npm, Python, Go o Rust con caché de dependencias, pruebas en matriz y publicación de artefactos. - [Keeping Git Repos Clean](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/keeping-git-repos-clean.md): Previene, detecta y corrige la subida a git de secretos (.env, API tokens, credenciales) y artefactos dev (builds, BD de trabajo, editor/SO). Cubre .gitignore (por qué no deja de trackear), git rm --cached, auditoría, historial y rotación. - [Building Python Mcp Servers](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/building-python-mcp-servers.md): Crea servidores MCP robustos en Python con FastMCP: diseño de herramientas, contratos de error, trabajo bloqueante, subprocesos/CLI, distribución, pruebas e inyección de prompts. Úsalo al escribir, exponer, depurar o probar servidores MCP. ## Skills relacionadas - [Building Python Mcp Servers](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/building-python-mcp-servers.md): Crea servidores MCP robustos en Python con FastMCP: diseño de herramientas, contratos de error, trabajo bloqueante, subprocesos/CLI, distribución, pruebas e inyección de prompts. Úsalo al escribir, exponer, depurar o probar servidores MCP. - [Concurrent Branches](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/concurrent-branches.md): Resuelve conflictos de merge con varias ramas abiertas: unión, recomputación y reconstrucción como resoluciones correctas; artefactos generados, serialización no determinista e IDs renumerados; un auto-merge limpio no es un test que pasa. - [Keeping Git Repos Clean](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/keeping-git-repos-clean.md): Previene, detecta y corrige la subida a git de secretos (.env, API tokens, credenciales) y artefactos dev (builds, BD de trabajo, editor/SO). Cubre .gitignore (por qué no deja de trackear), git rm --cached, auditoría, historial y rotación. - [Managing Python Releases](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/managing-python-releases.md): Gestiona lanzamientos de librerías Python: versionado semántico, changelog (Keep a Changelog), automatización con GitHub Actions y deprecaciones. Úsalo al planificar lanzamientos, escribir changelogs o comunicar breaking changes. - [Reproducing Ci Locally](https://skillsagentes.com/skills/kajisho5/ffmpeg-skill/reproducing-ci-locally.md): Reproduce el gate de CI en local a partir del workflow: comando, rutas, marcadores y entorno exactos; desbloquea pasos cortocircuitados, fija la versión del linter que resuelve CI y confirma que el run es verde. --- Skills Agentes · [Índice de páginas en markdown](https://skillsagentes.com/sitemap.md) · [Inicio](https://skillsagentes.com/index.md)