Repository navigation
residue v0.0.12
Install
curl -fsSL https://residue.whiteheap.com/install.sh | sh
brew install whiteheap/tap/residueEither one; every other way is in docs/installation.md. The script also turns on automatic cleanup, which deletes worktrees without asking; residue restore brings each back for 30 days. To install without it, run curl -fsSL https://residue.whiteheap.com/install.sh | sh -s -- --no-service. Homebrew installs without it. Or download the file for your machine below. The packages and the FreeBSD archives arrive within half an hour of the release.
| Machine | File |
|---|---|
| macOS, Apple silicon | residue-v0.0.12-aarch64-apple-darwin.tar.gz |
| macOS, Intel | residue-v0.0.12-x86_64-apple-darwin.tar.gz |
| Linux, static (musl), x86_64 | residue-v0.0.12-x86_64-unknown-linux-musl.tar.gz |
| Linux, static (musl), arm64 | residue-v0.0.12-aarch64-unknown-linux-musl.tar.gz |
| Debian, Ubuntu | residue_amd64.deb, residue_arm64.deb |
| Fedora, RHEL | residue.x86_64.rpm, residue.aarch64.rpm |
| Arch Linux | residue-x86_64.pkg.tar.zst, residue-aarch64.pkg.tar.zst |
| FreeBSD, x86_64 | residue-v0.0.12-x86_64-unknown-freebsd.tar.gz |
| FreeBSD, aarch64 | residue-v0.0.12-aarch64-unknown-freebsd.tar.gz |
Check a download: shasum -a 256 -c SHA256SUMS --ignore-missing, then gh attestation verify FILE --repo whiteheap/residue-cli.
The Mac app: https://github.com/whiteheap/residue-ui/releases
Launch hardening: residue keeps more worktrees that are in use or hold what it cannot rebuild, stops on a mistyped command or an old Git before anything runs, replaces only the service you looked at, and comes with a skill for coding agents.
- An
.envinside a build folder is kept. An environment file inside an ignored directory a rule matches, such asbuild/.envordist/.env.production, now keeps its worktree, as an ignored.envat the top always has:ignored: build/.env. Templates (.env.example) and files innode_modulesand virtual environments do not count, and a rule naming the file itself (git config --add residue.regenerable build/.env) lets residue delete it (#158). - A bare repository's HEAD branch is kept. In the layout
git clone --bareand then agit worktree addper branch makes, the worktree on the branch the bare repository's HEAD names is its main checkout in all but name. Theheadcheck now blocks it:on main, the branch the bare repository's HEAD names; kept like a main checkout(#156). residue service uninstallandstatustake no arguments.residue service uninstall --dry-rununinstalled the service; now any argument but--helpis a usage error and nothing runs (#156).- A mistyped command is a usage error.
residue cleanprintedcould not inspect: clean: No such file or directoryand exited 0; now it saysresidue: clean is not a command, and there is no folder named clean here; commands: scan, explain, remove, restore, stats, watch, service(#156). residuein the home folder scans the usual code folders, asrestoredoes, and says which; it no longer walks the whole home folder. A PATH,~included, is scanned as given (#156).- An old Git stops
service installandwatch --oncebefore they start, and the install script before it adds a package repository or asks for a password. A runningwatchsays so once and waits for a newer Git, instead of failing every pass (#156). service installand the install script say what automatic cleanup removes, that it deletes without the trash and for how longresidue restorebrings a worktree back, and how to preview it (#156).- A skill for coding agents.
npx skills add whiteheap/residue-cli --skill residue -ggives Claude Code, Codex and other agents the steps to remove the worktree they created once a task is done, and what to do when residue keeps it (skills/residue/SKILL.md, #163). service install --expectreplaces only the service you looked at.residue service statusprints a fingerprint of the service. Given it,installwrites nothing and exits 4 when the service changed since, so the Mac app can no longer save over a change a terminal orbrew upgrademade while its dialog was open. Installs and uninstalls now run one at a time (#162).- A process in a worktree keeps it in use whatever characters its path has, such as é, 漢字, an emoji or a backslash, in the worktree's name or a folder above it. On macOS, residue now matches the names lsof prints escaped. Where it cannot be sure how lsof prints a path, activity is
unknown, which keeps the worktree. On FreeBSD, residue reads each process's working directory, executable and open files from the kernel (kern.proc.filedesc), byte for byte, instead of from procstat (#157). - On illumos, an open file whose path the kernel cannot give still keeps its worktree in use: residue finds the file by its inode. Where it cannot look, activity is
unknown, and the detail names the process. A deleted file does not count (#160). - A worktree that holds a very long path is kept. Each probe names a path whole only below a length: 1,004 bytes on macOS, 4,096 on Linux and 1,024 elsewhere. A worktree with a path that long anywhere inside it is now
unknown(long_path), so it is never a candidate, and says how long that path is. So is one whose paths would reach that length where residue deletes it, at its hidden name or in the trash, so the plan never lists a worktree that the check after the move then keeps (#165). - On FreeBSD, residue also reads the files each process has mapped into memory (
kern.proc.vmmap). A file a process holds that the kernel cannot name is placed by its inode, as on illumos: in use when the worktree holds it,unknown(unnamed_file) when residue cannot tell. A process running as your user that the kernel will not show makes activityunknown(probe_failed), and the reason names the process, as every one does withsecurity.bsd.unprivileged_proc_debugat 0. A process the kernel shows only to root whatever the settings, such as ssh-agent or the process sshd keeps for each login, is not inspected, as on Linux (#165). - A removal Git stopped partway says what is left. When
git worktree removefails after Git has dropped the worktree from its list, the reason says what is left at the hidden name and ends with the removal record'sresidue restore, instead of a line that moves it back. That restore recreates the worktree and notes the folder left behind, for you to look through and delete (#165). - For scripts and integrations (changed from 0.0.11):
- With no command first, the first PATH, the first argument before
--that is neither an option nor an option's value, exits 2 when it has no/and names nothing that exists (residue clean,residue --json clean); a command there says it goes first (residue --json scan).residue -- cleanstill scans./clean. scanwith no PATH, run from the home folder or above it, scans the usual code folders there and notes it on stderr (residue: note: scanned only the usual code folders in your home folder (...)); with none of them, or from/with HOME unset or/, it exits 2.service statusandservice uninstallwith any argument but-hor--helpexit 2.- On a Git older than 2.36,
service installexits 1 withresidue: git X.Y is too old; residue needs git 2.36 or later, andwatch --onceexits 1 with<time> residue watch: git X.Y is too old; residue needs git 2.36 or later. A runningwatchlogs<time> residue watch: git X.Y is too old; residue needs git 2.36 or later; checking again every 5 minutesonce, keeps running, and once Git is new enough logsgit X.Y is new enough now; startingand starts. - The
headcheck blocks the worktree on the branch a bare repository's HEAD names (statusblock), and isunknownwhen that HEAD cannot be read. service installprintsremoves:,deletes:andpreview:lines beforecheck:.watch --oncelogs one line when its pass removes, or would remove, nothing, saying why (nothing to remove now: ..., orno removal this pass: each worktree it tried was kept, as logged above).- An ignored entry a rule matches is
"regenerable": falsewhen it holds an environment file no rule names, and theignoredcheck blocks:ignored entries not known to be regenerable: build/ (2 files, 18 B; build/.env is an environment file no rule names). - The scan hint names the preview first:
Automatic cleanup is off. Preview it: residue watch --once --dry-run · turn it on: residue service install. service statusprintsfingerprint: HEX, 32 lowercase hex digits, after what the service runs and beforefreed:, also when no service is installed (exit 3);fingerprint: unknown (REASON)when it cannot read the service file. The first line and thelog:line are unchanged.service install --expect FINGERPRINTand--expect noneare new. When the service is not the one named,installwrites nothing, prints one line on stderr (residue service install: the service changed since that fingerprint was read, or another residue release printed it; nothing changed. Read it again: residue service status, or fornone,residue service install: a service or its file exists, and --expect none installs only when neither does; nothing changed. See it: residue service status), and exits 4. A value that is neithernonenor 32 lowercase hex digits, a missing value or a second--expectexits 2. A service it cannot read to compare, or a folder it cannot lock, exits 1. Without--expect,installis unchanged but for the lock below.service installandservice uninstalllock the folder of the service file (~/Library/LaunchAgents,~/.config/systemd/user). One that finds it locked printsresidue service install: waiting for another residue service install or uninstall to end(oruninstall), waits up to 30 seconds, then exits 1. Where the folder cannot be locked, a command without--expectwarns on stderr and runs as before.activity.reasongains the valuesunmatchable_path,unnamed_fileandlong_path.- On FreeBSD,
activity.backendissysctl, notprocstat. - A
removeresult that isfailedafter Git dropped the worktree from its list has noRecover with:line. Itsreasonsays what is left at the hidden name and ends withits removal record keeps its HEAD and reflog: residue restore RECORD, and that restore'snotesaddwhat its removal did not delete is still at PATH; residue leaves it there for you to look through and delete.
- With no command first, the first PATH, the first argument before
Details
- The activity check now runs after the walk that sizes the worktree, at the plan and again after the move aside, with the probe's snapshot taken before the walk as before. The walk reports the longest path it listed, and the inode of each file another process holds unnamed that it finds, so on illumos and FreeBSD such a file is looked for without a second walk; the check that no process uses the worktree of an unfinished removal record (
removed::unheld) runs that walk itself, so a long path there isunknown(long_path) too. A walk that could not read an entry leaves such a fileunknown(unnamed_file). A path as long as the probe's limit makes activityunknown(long_path); a use the probe saw still comes first. Tests:activity::tests(the limit, the detail, and how an unnamed file is judged),fsinfo::tests::the_walk_reports_its_longest_path_and_each_wanted_file_it_lists, andscripts/contract-check.shsection 17,activity-long-path. - On FreeBSD, each process's
kern.proc.vmmapis read after itskern.proc.filedesc, both into buffers kept for the whole snapshot.psnow also lists each process's real, effective and saved user ids, and its flags, so a process the kernel will not show is a failure only when all three ids are yours and it is not one the kernel shows to root alone (P_SUGID,P2_NOTRACE, or init). Tests:activity::tests(the kinfo and vmentry records, thepsline, which processes the kernel shows to root alone) andscripts/contract-check.shsection 18,activity-proc-debug-off, which runs as root in the FreeBSD job and also runsexplainwhile the user has an ssh-agent and an SSH session to localhost. - The plan holds each worktree's longest path against the probe's limit at the longest path the worktree itself can have where residue deletes it (
remove::deleted_at):.NAME.residue-removing-PIDwith a PID of up to ten digits, or the trash entry, whichever is longer, and the worktree's own path for one already in the trash. Tests:remove::tests,activity::tests, andscripts/contract-check.shsection 17b,activity-near-path-limit, where a worktree 10 bytes short of the limit is refused at the plan, with nothing moved. scripts/recovery-check.shgainsopened-before-move(a file opened at the top of the worktree right before the move keeps it, put back) andremove-fails-unlisted(the reason, the folder left aside, and the restore that recreates the worktree and notes it).- The bare repository's HEAD branch is read once per repository, with
git symbolic-ref -q HEADin its Git directory, and only when the main entry is bare. A detached HEAD keeps nothing; a HEAD residue cannot read makes the checkunknown, which keeps the worktree.scan,explain,remove's plan and its re-check just before each deletion all apply it. Tests:scripts/contract-check.shsection 15,bare-head-branch: scan, explain and remove keep the worktree onmainwithheadblocking, and remove the one onfeat; it fails on 0.0.11, where scan listedmainas a candidate. residue servicedecides what to run from its arguments in one function, before anything touches launchd or systemd:-hor--helpanywhere prints the help, any other extra argument tostatusoruninstallprintsresidue service uninstall: unexpected argument XandRun \residue service --help` for usage.`, exit 2. In-crate tests cover every case without reaching either service manager.service install --expect: the fingerprint is FNV-1a, 128 bits, over the residue release, launchd's loaded state on macOS, the service file's bytes or that there is none, and the path the binary the file starts resolves to or that it names none, each part with its length. It covers the resolved path, not the binary's version or inode:brew upgradechanges the path under the same link, and the Mac app copies its own residue over the binary its service starts right before each save, which would make every such save a mismatch. The release covers the binary that writes the new service, which is the one it then starts.installcompares once before any work, to stop early, and again right before the write, under an exclusive flock(2) on the folder of the service file that it holds until it returns;uninstalltakes the same lock. It locks the folder, not a file of its own, so every residue that writes that service file takes the same lock, whateverXDG_STATE_HOMEsays. Not covered: a writer that takes no lock, such as an older residue or an editor, between the last comparison and the write; a systemd drop-in, which residue never writes; and an upgrade in place (the same path, a new release) of a binary that the install replaces with another one, which leaves the fingerprint alone but does not change what the install writes. In-crate tests cover the fingerprint (each part changes it, a byte moved between parts too, and the same service gives the same one),--expect's parsing and decision, and the lock's wait and time limit..github/service-smoke.shstep 3 runs a matching, a stale and anoneinstall on macOS and Linux and checks that a refused one leaves the service file as it was.- A first word that is not a command was taken as a PATH to scan, and so was the first PATH after options (
residue --json clean). Now, when no command comes first and that PATH has no/and names nothing that exists, it is a usage error listing the commands; a command named after an option (residue --json scan) is told to go first. An option's value (--regenerable clean) is never taken for that PATH.residue .,residue ~/src, an existing folder's name andresidue -- cleanscan as before. A first argument that is not UTF-8 was dropped and.scanned in its place; it is now scanned as given. residueorresidue scanwith no PATH, from the home folder or above it, walked up to four levels of everything there. It now scans the folderswatchdefaults to that exist (~/src,~/code,~/Developer,~/Projectsand the restresidue watch --helplists), likerestore. With HOME unset or/, from/, it scans nothing and says in one line to name the folder:residue: no PATH given, and HOME names no home folder; name the folder that holds your repositories: residue scan PATH. The Mac app always passes its folders, so it is unchanged.residue watch --oncelogs one line when its pass removed nothing, or with--dry-runwould remove nothing, saying why: no linked worktrees, no branch landed or a check keeps those that did, and the free space of each disk against--min-free, to one decimal (the disk is 10.3% free (removes below 10%)). Every free-space figurewatchlogs takes more decimals when one would put it on the other side of--min-free:9.96% free, below 10%, never10.0% free, below 10%. A runningwatchdoes not log it each pass. A pass that tried removals and removed none saysno removal this pass: each worktree it tried was kept, as logged above.explainoutside a Git repository, or on a path that does not exist, says so in one line and what to pass in the next, instead of Git's error.no Git repositories foundsays how deep residue looked and suggests naming the folder.- The walk that sizes each ignored entry also notes the first environment file strictly inside it that is outside
.gitentries and package folders and that no rule names itself, at no extra file system cost. A directory a rule matches, or a collapsed directory whose every file a rule matches, is then judged kept, soscan,explain,remove's plan, its re-check before each deletion andwatchall keep the worktree. An environment file that is itself the ignored entry is judged by the rules, as before. Tests:policy::tests(which names are environment files; a rule names a file only by its name or its whole path) andfsinfo::tests::an_environment_file_inside_an_ignored_folder_is_found. residue --helpshowsresidue service install|status|uninstallandKept: branches, commits and the stash.residue service install --helpstarts by saying it takesresidue watch's options.watchchecks the Git version before its first pass.watch --onceon an old Git stops at once, exit 1, with the same message asscan. A runningwatch, the service, takes its lock, logs that message once and checks again every five minutes, then starts when Git is new enough; before, every pass's scan failed and was logged, and exiting instead would have had launchd and systemd start it again every minute. While it waits, an upgrade of residue still ends it, so the service starts the new release. When ascanorremovechild fails before its JSON, the log shows the child's stderr alone.- A work-landed removal's log line adds how many commits on no remote-tracking ref stay on the branch, when there are any:
; 2 commits on no remote-tracking ref stay on branch fix. - Low disk: a removal's space can take a while to show as free, such as while an APFS local snapshot holds it. A later low-disk pass counted that space as still needed and removed more. Now each pass counts what low-disk removals on that filesystem freed in the last 24 hours, and free space does not show yet, toward its target, and logs that it waits for it:
waiting for about 3.1 GB that removals freed in the last 24h to show up as free space (a filesystem snapshot can hold it). It never removes more than before. 24 hours is how long Time Machine keeps its local snapshots. Right after a pass's removals it reads free space again, so only what does not show by then waits, and each later reading takes off only what free space gained since the one before. It cannot tell space that showed up and was used again between two readings from space that never showed, and waits for that until the 24 hours pass. The count is cleared once the filesystem is above--min-free. - The install script stops before it changes anything on Windows (
residue does not run on Windows; run this inside WSL), on a CPU with no build (it links to building from source) and without curl. On the apt path it stops before writing under/etcwhen apt offers a Git older than 2.36, and namessudo add-apt-repository ppa:git-core/ppafor Ubuntu 22.04. It skipsservice install, saying why, whengit --versionis older than 2.36. Whenservice installfails, it says to fix what it named and run it again..github/workflows/install.ymlgainsrefusals(no curl, an armv7l CPU, Ubuntu 22.04's Git on the apt and the archive paths) andwindows(Git Bash). - A build for a system other than Unix stops at compile time with
residue runs on Unix-like systems only; on Windows, use WSL. - Release notes start with how to install: the script, Homebrew, and a table of the file for each machine, with the check commands and a link to the Mac app.