All notable changes to this project will be documented in this file.
This project adheres to Semantic Versioning.
[scan] dirsconfig key: an exhaustive list of scan roots, unlike[scan] extrawhich adds to home. Home is scanned only if you list it yourself, so you can scan just~/Codeinstead of all of home (#129).
0.12.0 — 2026-07-30
-
Vagrant boxes (
~/.vagrant.d/boxes) is now one of the built-in global caches, excluded when you opt in with[fixed_dirs] enabled = true(#117). -
[skip_paths]config section: name directories that Asimov should never search, alongside the built-in~/.Trashand~/Library. Thanks @lunaluxie! -
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. - The Homebrew formula installs the three pieces under one prefix (
bin/asimov,libexec/asimov/,share/asimov/) instead of a single script.brew install asimovandbrew services start asimovare unchanged.
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.