All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
asimov doctornow checks the data files and reports how many sentinels and fixed directories are loaded, so an incomplete install is diagnosed rather than just failing.
- Asimov is no longer a single file. The launcher lives at
bin/asimov, the logic inlib/asimov/*.sh, and the directory lists indata/*.tsv. Installed, those become<prefix>/bin/asimov,<prefix>/libexec/asimov/and<prefix>/share/asimov/. Behaviour is unchanged; every existing test passes untouched. - Sentinels, fixed directories and skip paths are now tab-separated data files instead of bash arrays. Each record carries its ecosystem or owning tool as a real field, so adding a pattern is a one-line edit with no bash syntax involved.
- The remote installer (
scripts/install-remote.sh) downloads a tarball instead of a single script, and installs under~/.local. - Release assets are now
asimov-<version>.tar.gzrather than a bareasimovscript.
0.11.0 — 2026-07-29
-
asimov doctorchecks an install rather than your projects: whichasimovyour shell actually runs (and flags an older one shadowing it onPATH), whether a schedule is installed and loaded, whether~/.cache/asimov/is readable and writable by you, whether the config file parses, and whethertmutilcan read exclusions at all. It is read-only — it prints the fix rather than applying it — and never executes anotherasimovbinary it finds, reading versions out of the file instead, because v0.3.0 ignores every argument and would start a real scan. Exits1if it finds anything (#122). -
UPGRADING.md: a section for upgrading from v0.3.0, the version most people have, covering the three leftovers that outlive it — the LaunchAgent, the old cellar, and a root-owned cache — in an order that works (#122). -
make test-system-bashruns the suite under the macOS system bash (3.2), which is what most users get —asimovstarts with#!/usr/bin/env bash, and a development machine usually has 5.x first onPATH.make test BASH_BIN=<path>pins any interpreter, andmake test BATS_JOBS=Nruns tests concurrently (needs GNU parallel). -
asimov prunereports Time Machine exclusions whose directory no longer exists, and compacts Asimov's own path cache. It is read-only with respect to Time Machine: stale entries live in the system-wide "sticky" list (tmutil addexclusion -p), which Asimov never writes, sopruneprints thesudo tmutil removeexclusion -pcommand rather than running it. Asimov's own exclusions are stored as an attribute on the directory itself and are therefore removed automatically when the directory is deleted — only sticky entries can outlive their directory (#38). -
README: a "What Asimov doesn't do" section, covering the three things Asimov is most often expected to do and doesn't — hide directories from Spotlight (a separate mechanism from Time Machine exclusions), shrink backups that already exist, and delete anything — plus how to verify a run reached your projects (#90, #45).
- CI now runs a matrix of both macOS versions × both bash versions (system 3.2 and Homebrew
5.x), with ShellCheck split into its own job so it runs once instead of once per cell.
All jobs run in parallel,
fail-fastis off so one failing combination cannot hide the others, and a newer push cancels an in-flight run for the same ref.
- An unreadable or unwritable file under
~/.cache/asimov/no longer kills the run. The cache is an optimisation, but a barecaton an unreadable state file failed underset -Eeu -o pipefailand aborted immediately, printing nothing butPermission denied. Every cache read and write is now guarded: Asimov warns once, names the reset command, and continues without the cache (#122). - A run as root no longer leaves root-owned files behind that break the next run as your own user — the cause of the failure above. The cache directory was chowned to the console user, but the state files written afterwards were not; they are now created before the chown, and appending never changes an existing file's owner (#122).
0.10.0 — 2026-07-26
First release under the project's new home, AsimovMac/asimov
— the original stevegrunwell/asimov
repository transferred to a dedicated organization (stars, history, and the old
URL preserved via GitHub's redirect). Folds in all fork work from v0.4.0 through
v0.8.0, after a v0.9.0-beta.1/beta.2 testing round.
- Scan directories beyond your home directory. Add a
[scan]section to~/.config/asimov/configwith oneextra =line per additional root (e.g.extra = /private/var/www); each is scanned on every run alongside home. Configured directories that don't exist are skipped with a warning rather than failing the run, and directories nested inside another scanned root are pruned to avoid traversing the same tree twice (#108) - Go's module cache (
~/go/pkg/mod) is now one of the built-in global caches (opt-in via[fixed_dirs] enabled = true). Excluding the cache root covers every dependency inside it in a singletmutilcall, instead of attempting eachvendor/directory individually (~11s apiece, and doomed — see below). Using a customGOPATH? Add it with[fixed_dirs] extra
- Project home is now
AsimovMac/asimov; release tooling, README, and install scripts retargeted accordingly
- Read-only directories are now detected and skipped up front with a clear
read-only, cannot be excludedmessage, instead of burning ~11s on atmutil addexclusioncall that can never succeed. A Time Machine exclusion is stored as an extended attribute on the item itself, so a0555directory can never take one — Go's module cache is deliberately read-only, which is why itsvendor/directories always failed tmutil'sPOSIXError(_nsError: …Code=22 "Invalid argument")dump no longer leaks into Asimov's output.tmutilprints it to stdout (not stderr), so the existing2>/dev/nullnever suppressed it; both streams are now silenced and Asimov prints its own warning instead- Corrected a long-standing misdiagnosis in the code comments: these failures were attributed to the
@in Go module version paths.@is irrelevant — a writable path containing@excludes fine. Read-only permissions were always the cause (regression tests now cover both)
0.8.0 — 2026-06-19
--no-read-cacheand--no-write-cacheflags that control the cache on two independent axes.--no-read-cacheignores all cached state (re-discovers and re-verifies everything, then rebuilds the cache);--no-write-cachereads the cache but persists nothing.--full-scanis now an alias for--no-read-cache, and--no-cachean alias for both together — both kept for backwards compatibility
--full-scannow re-evaluates everything: it ignores theexcludedandfailedstate too (not justpaths/mdfind_seen), so a path you manually un-excluded, or a previously failed path, is re-checked against thetmutil isexcludedground truth. Previously it still trusted theexcludedcache and skipped those paths--no-cachenow honors its documented contract — it reads and writes nothing. Previously it still read and wrote theexcluded/failedstate files--full-scanand--no-cacheare no longer mutually exclusive (combining them is well-defined: both reads and writes are skipped); the previous "mutually exclusive" error is removed
- Security: a wildcard sentinel value from
~/.config/asimov/config([sentinels] extra) was interpolated unquoted into afind -execdir sh -c …string, so a crafted value like*.x'; cmd; 'could run arbitrary commands during a scan. The glob is now passed as a positional argument and never re-parsed as shell code (globbing still works); built-in sentinels were unaffected - Persistent
failed-state (~/.cache/asimov/failed) is now deduplicated each run like the other caches, instead of growing by a duplicate line on every run that hits atmutilfailure (e.g. Go@-module paths) scripts/install.sh/scripts/uninstall.shnow resolveasimovand the plist relative to the script's own location (viaBASH_SOURCE) instead of the caller's working directory, somake install/uninstallno longer depend on being run from the repo root- Hardening:
asimovsetsumask 077so its temp files (which list the full home-directory layout) and its~/.cache/asimovstate are created private to the user --dry-runnow takes the exact same code path as a real run: it runs the read-onlytmutil isexcludedground-truth guard instead of stopping at the Spotlight cache, then printsWould exclude(and never persists state or callsaddexclusion). Previously the preview over-reported, listing directories that a real run would skip — most visibly a dependency dir already covered by a manually excluded ancestor (e.g.~/.nvm), since path-based Time Machine exclusions aren't reported by Spotlight (#19)make bump-formulano longer commits a bad Homebrew formula when the release tarball download fails. The oldcurl … | shasum | awkpipeline swallowed a failedcurl(pipeline exit status came fromawk), so a transient network error produced the SHA-256 of an empty string (e3b0c4…b855) — which slipped past the all-zeros guard and was committed/pushed to the tap, breakingbrew install. Now downloads to a temp file with an explicit failure check, rejects an empty tarball, enablespipefail, and rejects the empty-string hash. Surfaced during the 0.7.0 release
0.7.0 — 2026-06-18
- moonrepo: exclude
.moon/cachewhen aworkspace.ymlsits beside it - CONTRIBUTING.md: troubleshooting entry for
make prep-releasefailing on a dirty tree when the uncommitted changes ARE the release content (manual fallback)
- README: surface user-defined sentinels prominently — a new Add your own patterns subsection explains the
directory sentinelformat with examples, and a pointer under the ecosystems table sends readers there. Custom exclusion patterns were already supported via[sentinels] extra; this just makes them discoverable - Makefile pins
ghtodjango23/asimovviaexport GH_REPO, so a configuredupstreamremote (e.g.stevegrunwell/asimov) can no longer hijack release-flow gh calls. Surfaced during the 0.6.4 release whenmake ship-formula404'd against the upstreamrelease.ymlandgh pr create --fillfailed to compute a diff range make verify-releasenow invokes$(brew --prefix asimov)/bin/asimov --versiondirectly instead of bareasimov, so an older binary earlier onPATHno longer hides the freshly-installed version
- README badges restored — the 0.6.4 README rewrite stripped the
image syntax, leaving plain text links instead of the Tests / Latest release / Stars / macOS / License / Shell shields
0.6.4 — 2026-05-01
make prep-release VERSION=X.Y.Z(backed byscripts/prep-release.sh) — bumpsASIMOV_VERSION, promotes[Unreleased]in CHANGELOG with compare links, runsmake check, and commits on arelease/X.Y.Zbranch. Removes the two manual edit steps that were the most error-prone part of the release flowmake ship-formula— waits forrelease.ymlto finish, runsbump-formula, and pushes the Homebrew tap. Replaces thesleep 6+gh run watchrecipe that previously lived only in CONTRIBUTING.mdmake verify-release—brew upgrade asimov(or install) +asimov --version
- CONTRIBUTING.md release flow trimmed from a 4-step prose narrative to ~10 commands; the orchestration now lives in Makefile targets so an AI agent (or human) can execute the release end-to-end without re-deriving sed scripts or watch loops
- README rewritten with an outcome-first hook, worktree/AI-coding angle, and a "What you'll see" sample run; install moved above the explainer to lower friction, and the
--statsflag is now documented in the Usage table
0.6.3 — 2026-04-21
- README: added "Latest release" and "Stars" badges alongside the existing Tests / macOS / License / Shell badges so progress and social proof are visible at a glance
0.6.2 — 2026-04-20
SECURITY.md: replaced invaliddjango23@github.qkg1.topcontact with the project maintainer's GitHub noreply address so vulnerability reports no longer bounce- README: Uninstall section used legacy
launchctl unload; replaced with modernlaunchctl bootoutto match the Schedule section
0.6.1 — 2026-04-20
- README: moved Usage section above Schedule so the core command reference comes before scheduling details
0.6.0 — 2026-04-20
- Persistent path cache (
~/.cache/asimov/paths) that stores discovered dependency paths across runs. Subsequent runs skip the fullfindtraversal entirely, reducing runtime from ~75s to ~1-2s on typical home directories - Incremental Spotlight discovery via
mdfindto find newly created projects without traversing the filesystem --full-scanflag to force a full filesystem scan, ignoring any existing cache--no-cacheflag to run a full scan without reading or writing cache--statsflag to show per-directory sizes and a total-space summary (opt-in, replaces previous always-on behaviour)- Optional positional
[directory]argument to scope the scan to a specific directory instead of the full home directory make benchtarget to compare dry-run timing againsttests/fixture/across versionsmake bench-hometarget to compare dry-run timing against the real home directory across versionstests/fixture/directory with representative project structures for correctness checks and benchmarking- Persistent excluded-path state (
~/.cache/asimov/excluded) that tracks successfultmutil addexclusioncalls across runs. Handles interrupted runs and Spotlight indexing delays — paths excluded in a previous run are never re-excluded, even if Spotlight hasn't indexed them yet - Persistent failed-path state (
~/.cache/asimov/failed) that remembers paths wheretmutil addexclusionfails (e.g. Go module paths with@characters). Failed paths are automatically skipped on subsequent runs instead of retrying; use--full-scanto retry - Persistent mdfind-seen cache (
~/.cache/asimov/mdfind_seen) that remembers all candidates checked by Spotlight discovery. Directories without sentinels (false positives) are not re-checked on subsequent runs, eliminating ~1,200 redundant sentinel lookups per cached run
- Default run with an existing cache now uses the cache instead of a full
findtraversal; use--full-scanto force re-traversal - Directory sizes and "totalling" summary are no longer computed by default; use
--statsto enable them. Removes thedu -skcall per matched directory, significantly reducing wall-clock time on large home directories. - Already-excluded paths are now filtered via a fast cache lookup (
grep) instead of adding per-path-not -pruneclauses tofind. Removes the O(directories × excluded_paths) overhead that dominated scan time on large home directories. Speed improvements of 37% tmutil addexclusionuses per-path calls instead of batch to avoid wasting minutes when a single path fails (e.g. Go module paths with@characters). Batch mode provided no speed benefit — tmutil processes paths sequentially at ~11s each regardless- Paths that are descendants of fixed dirs (e.g.
~/.cache) are skipped during sentinel-based exclusion since the fixed dir will be excluded unconditionally - Nested dependency paths are automatically deduplicated — if a parent directory is excluded, its descendants are skipped since Time Machine exclusions are recursive
- Full scan now streams
findoutput throughteeto write the cache incrementally; an interrupted scan leaves a partial cache that accelerates the next run - Spotlight incremental discovery now bulk-filters cached paths with a single
grep -Fxvfinstead of onegrepsubprocess per candidate, and uses bash parameter expansion instead ofdirname/basenamesubprocesses — eliminates ~27,000 subprocess spawns on typical home directories - Spotlight discovery now prefix-filters candidates nested under cached paths before sentinel checking — eliminates thousands of redundant sentinel lookups (e.g.
node_modules/dep/node_modulesunder an already-cachednode_modules) - Spotlight discovery uses temp files instead of bash string variables for large datasets — avoids slow
printfon 15K-line strings - Spotlight discovery now runs descendant filter before exact-match filter and uses
sort+comminstead ofgrep -Fxvffor set difference — BSDgrep -Fxvfis O(n×m) and took 35s on 15K candidates;sort+commis O(n log n) and takes <1s - Excluded-path filtering now uses a single bulk
grep -Fxvfinstead of per-path subprocess spawns - Default branch renamed to
main(wasdevelop); branch protection requires signed commits + passing CI - CI workflow now runs on every PR (including from forks), not just specific base branches
make releaseis now strict: requires clean tree, must be onmain, signed tags only- Added
make bump-formulato update the Homebrew tap (django23/homebrew-tap) after a release
0.5.0 — 2026-03-02
--quietflag to suppress all non-error output--verboseflag to show already-excluded directories (hidden by default)- Config file support (
~/.config/asimov/config) for enabling fixed dirs, adding extra dirs/sentinels, and disabling built-in sentinels - Curl-based installer (
scripts/install-remote.sh) for non-Homebrew users - Unit tests for
format_size_kb()boundary values - Flag combination tests (
--dry-run --verbose,--dry-run --quiet) - GitHub issue/PR templates
SECURITY.mdwith private vulnerability reporting instructionsUPGRADING.mdwith migration guide from v0.4.x and original asimov
- BREAKING: Fixed dirs (global caches) are now opt-in via config file (previously always on)
- BREAKING: LaunchAgent label renamed from
com.stevegrunwell.asimovtocom.django23.asimov - Colors are now disabled when stdout is not a terminal (e.g. launchd, pipes)
tmutil isexcludedcheck removed from default path (now--verbose-only); mdfind optimization already prunes excluded pathsrecord_excluded_path()callsduonce instead of twice per pathformat_size_kb()uses bash arithmetic instead of spawningawk- Simplified
print_exclusion_summary()to a single printf with early return - Removed stale
@versionheader comment - Removed unnecessary defensive guards on always-initialized variables
- Mock tmutil now idempotent (matches real tmutil behavior)
- README rewritten with concise install-first structure
- Updated all repo URLs from stevegrunwell/asimov to django23/asimov
tmutil isexcludedper-path check in default (non-verbose) mode
0.4.0 — 2025-02-20
- Validation that
ASIMOV_ROOTexists and is a directory before running (clear error and exit 1 if not) - Test for project paths containing spaces; test for dry-run summary format (count and size)
--helpand--versionoptions; unknown options exit with an error and usage message--dry-runoption to print what would be excluded without callingtmutil- Support glob patterns in sentinel definitions, enabling wildcards like
*.xcodeproj(stevegrunwell/asimov#64, props @mdab121) - Exclude Xcode DerivedData when
*.xcodeprojis present (stevegrunwell/asimov#64, props @mdab121) - Exclude well-known global cache directories (
~/.cache,~/.gradle/caches,~/.m2/repository,~/.npm/_cacache,~/.nuget/packages,~/.kube/cache, etc.) without requiring sentinel files (inspired by stevegrunwell/asimov#69, props @pkuczynski) - Display a summary of total count and size of newly excluded directories at the end of each run (inspired by stevegrunwell/asimov#84, props @Vadorequest)
- Exclude Next.js build cache (
.next) - Exclude Nuxt build cache (
.nuxt) - Exclude Angular CLI cache (
.angular) - Exclude SvelteKit build output (
.svelte-kit) - Exclude Turborepo cache (
.turbo) - Exclude Yarn Berry cache (
.yarn) - Exclude Leiningen/Clojure CLI
targetand.cpcachedirectories - Exclude Shadow-CLJS cache (
.shadow-cljs) - Exclude Python virtualenv
venvwithpyproject.tomlsentinel - Exclude PEP 582/PDM local packages (
__pypackages__) - Exclude Elixir/Mix standard
_builddirectory - Exclude Terraform providers/modules (
.terraformwith.terraform.lock.hcl) - Exclude direnv output (
.direnv) - Exclude OCaml/Dune build output (
_build) - Exclude Zig build cache (
.zig-cache) and output (zig-out) - Exclude Elm packages (
elm-stuff) - Exclude Godot 4 editor cache (
.godot) - Exclude R renv environment (
renv) - Exclude .NET build output (
bin,obj) when*.csprojor*.fsprojproject files are present (inspired by stevegrunwell/asimov#87, props @guigomesa) - Added
make installandmake uninstalltargets for streamlined setup and removal ([#35], props @sylver) - Added
scripts/uninstall.shto cleanly remove Asimov and its launchd schedule ([#35], props @sylver) - Added common interval reference comments to
com.stevegrunwell.asimov.plist([#35], props @sylver) - Dropped macOS 13 from the CI test matrix (runner was retired December 2025)
- Use
ASIMOV_ROOTinASIMOV_SKIP_PATHSandASIMOV_FIXED_DIRSso skip/fixed paths are correct when running as root (e.g. launchd) - Refactor: extract
record_excluded_path()for DRY size logging and output;resolve_asimov_root(),build_find_skip_params(),build_find_vendor_params(),print_exclusion_summary(),format_size_kb(); renameexclude_filetoexclude_paths_from_stdin; add named constants for size and colors - Skip non-directory paths in
exclude_paths_from_stdin(avoids failures if a path disappears between find and processing) - Use
printfinstead ofecho -ein install/uninstall scripts for portability - Skip directories already excluded from Time Machine backups for faster subsequent runs (inspired by stevegrunwell/asimov#97, props @VladRassokhin)
- Migrated test suite from PHP/PHPUnit to Bats (Bash Automated Testing System), removing the PHP dependency for contributors
- Replaced Travis CI pipeline with GitHub Actions (macOS 14 + 15 matrix)
- Replaced PHP
tmutilmock with a pure bash implementation - Moved install script to
scripts/install.shwith shared variables, now copies binary instead of symlinking ([#35], props @sylver)
- Handle
tmutilerrors gracefully instead of crashing; paths that fail exclusion are skipped with a warning (stevegrunwell/asimov#101, stevegrunwell/asimov#86) - Detect the logged-in user's home directory when running as root, fixing
brew servicesandsudoinvocations that would search/var/rootinstead (stevegrunwell/asimov#72) - Fixed duplicate Gradle sentinel entries in the sentinels list
- Fixed typo in comment ("decendents" → "descendants")
- Removed PHP test infrastructure (
composer.json,phpunit.xml.dist, and PHP test files) - Removed Travis CI configuration (
.travis.yml)
- Added Homebrew support 🙌 ([#34], props @Dids)
- Exclude Bower dependencies ([#22], props @moezzie)
- Exclude Maven builds ([#30], props @bertschneider)
- Exclude Stack dependencies ([#32], props @alex-kononovich)
- Exclude Carthage dependencies ([#37], props @qvacua)
- Exclude CocoaPods dependencies and Swift builds ([#43], props @slashmo)
- Exclude Bundler, Cargo, and Dart dependencies ([#56])
- Define a Travis CI pipeline for Asimov ([#20])
- Add an automated test suite using PHPUnit ([#31])
- Removed an extraneous
read -r path, which was causing the first match to be skipped ([#15], props @rowanbeentje) - Use the full system path when running
chmodininstall.sh([#33], props @ko-dever)
- The size of the excluded directories are now included in the Asimov output ([#16], props @rowanbeentje)
- Switch to using find's -prune switch to exclude match subdirectories for speed, and exclude ~/Library folder from searches ([#17], props @rowanbeentje)
- Rework the
findcommand and path variables so thatfindis only run once however many FILEPATHS are set ([#18], @props @rowanbeentje, yet again 😉) Fix incorrect directory pruning, simplify path handling ([#36], props @rwe) - Recommend cloning via HTTPS rather than SSH for manual installations ([#52], props @Artoria2e5)
- Don't look for matches in
~/.Trash([#55])
- Bundle the script with
com.stevegrunwell.asimov.plist, enabling Asimov to be scheduled to run daily. Users can set this up in a single step by running the newinstall.shscript. Added a formal change log to the repository. ([#5])
- Fixed pathing issue when resolving the script directory for
install.sh. Props @morganestes. ([#7])
- Change the scope of Asimov to find matching directories within the current user's home directory, not just
~/Sites. Props to @vitch for catching this! ([#10]).
Initial public release.