0196: Name protected paths for removal
Status: accepted (2026-10-02) · Scope: CLI utility (rm) · Amends: 0195
Context
Section titled “Context”ADR 0195 refused every symbolic link inside a recursive target and every
protected path, with no bypass. The first real cleanup hit both. A throwaway
copy of an application linked back to the source checkout’s node_modules
and scripts folder, and the copy had duplicated a .credentials folder. The
command could not remove the copy, so the only route left was shell rm or
unlink, which skip every check the command exists to run. A guard whose
refusals leave only the unguarded tool teaches agents to reach for that tool.
Alternatives considered
Section titled “Alternatives considered”- Keep the absolute refusal. Agents then fall back to shell
rmon exactly the paths that most need checking. - A general
--forceflag. It would switch off every protection at once, including protections the caller never meant to cross. - An override only for credentials. Throwaway clones carry
.gittoo, and the same failure would recur there.
Decision
Section titled “Decision”Links inside a recursive target are removed as links. Node’s recursive removal unlinks a link and never follows it, so the link’s target is never read or changed. The report lists each link with what it points to. A link at the target or in any ancestor is still refused, because it would redirect which files are removed.
Protected paths (.git, .harnery, .credentials, .ssh, .gnupg, and
bare repository directories) may be removed when the caller names each one
with --allow-protected <path>. Every other check still applies. Each named
path must:
- be the protected path itself, strictly inside
--root, reached without symlinks; - actually be removed by one of the targets, so a typo cannot pass silently;
- not be, or sit inside, the
.gitor.harneryof the live workspace (the coordination root and the checkout the command runs in).
A protected path inside an allowed one still needs its own name. Every
repository directory under an allowed path must have no branch or tag commit
missing from its remotes; a repository that cannot be checked is refused.
The report’s protected field lists the named paths, and refusal messages
show the flag to use.
Result and limits
Section titled “Result and limits”The harnery.removal/v1 report gains protected and links. The
unpushed-history check reads local remote-tracking refs, so a commit pushed
from another clone and never fetched here counts as unpushed, which errs
toward refusing. Stashes and reflog-only commits are not checked. Removing a
.git keeps the working files and loses the index and any stash.
Amendment (2026-10-03): artifact workspaces
Section titled “Amendment (2026-10-03): artifact workspaces”Files strictly inside one artifact workspace (.harnery/artifacts/<id>/...)
can be removed, and --root may be the workspace itself. Workspaces hold
local outputs such as browser profiles and test credentials, which an agent
should be able to clear while keeping the screenshots it needs. The workspace
directory, its .harnery-artifact.json record, and all other Harnery state
stay protected; the workspace’s lifecycle belongs to the artifact commands.
A path under protected state now names the containing protected directory
instead of suggesting an --allow-protected override that could not apply.