amphetamine 0.1.0

Reclaim memory and win scheduler contention on Apple Silicon, safely.
amphetamine-0.1.0 is not a library.

Amphetamine

A Rust CLI that frees memory and reduces CPU contention on Apple Silicon, so your editor gets the machine's attention.

It closes only the apps you have named, clears only caches that belong to installed applications, and refuses to make any change it cannot undo.

What it actually does, and what it can't

Being straight about this matters more than the pitch:

There is no overclocking. Apple Silicon exposes no user-space clock control — no cpufreq, no multiplier, nothing, SIP enabled or not. The only performance selector is the Energy Mode in System Settings, and amph status will tell you if you are not already on High Power. If nothing is thermally throttling you, there is no clock speed to win back, and any tool claiming otherwise is lying.

What is real is contention and memory pressure:

Lever Mechanism Honest effect
Closing apps Quit request, as if you pressed Cmd-Q Frees RAM and, critically, drains swap
Clearing caches Age-gated file deletion Frees disk, reduces cache-warming I/O
Deprioritising nice Rival processes get a weaker claim on the CPU
Idle-sleep hold IOKit power assertion Long builds are not interrupted

Swap is usually the real culprit. Free RAM is a poor health signal; swap is a better one. Pages pushed to swap during past pressure stay there, and every touch of one is a disk read. Swap only drains as the processes owning those pages exit — no amount of free RAM, and no purge, brings it back. That is why closing apps helps even on a 128 GB machine.

nice is not efficiency-core parking. PRIO_DARWIN_PROCESS, the mechanism behind taskpolicy -b, silently does nothing when aimed at another process on current macOS: it reports success and changes no state. Amphetamine uses nice, which measurably works, and does not claim to do more.

Install

cargo build --release
cp target/release/amph /usr/local/bin/

Requires macOS on Apple Silicon and a recent stable Rust.

Use

amph                     # same as `amph status`
amph status              # read-only: memory, swap, thermal, heaviest apps
amph pick                # choose what to close, interactively
amph boost --dry-run     # exactly what would be closed and cleared
amph boost               # do it
amph focus --for 90      # a 90-minute focus session
amph restore             # undo any leftover deprioritisation

The first run writes ~/.config/amphetamine/config.toml with empty lists and changes nothing. That is the design, not an oversight: Amphetamine closes an app only after you name it.

Choosing what to close

You never have to open the config by hand.

amph pick                # tick apps from what's running, ranked by memory
amph add Slack Spotify   # or name them directly
amph add -d Dropbox      # -d: deprioritise during focus instead
amph add -p Notion       # -p: protect, never close
amph rm Spotify          # remove from a list
amph config show         # every setting, annotated with what's running

amph pick opens a checklist of running apps pre-ticked to match your current config, so it reads as an editor rather than a one-way import. Apps that are protected are never offered, and entries for apps that aren't running are left untouched rather than silently dropped.

Names match a bundle ID, an app name, or the last component of a bundle ID, case-insensitively, and are stored using the app's own spelling — amph add slack records Slack. Adding something the built-in denylist protects is refused with the reason rather than accepted as a line that could never fire:

$ amph add Cursor Docker
close list
  ! Cursor not added — protected (Cursor) — it would never be touched
  ! Docker not added — protected (Docker) — it would never be touched

Edits are made in place with toml_edit, so the config's explanatory comments survive; each result is parsed back and validated before it replaces the file, and the swap is atomic, so a failed edit leaves the previous config whole.

For hand-editing, amph config edit opens $EDITOR and amph config raw prints the file as-is.

[apps]
close = ["Slack", "Spotify", "Messages"]

[caches]
min_age_days = 7

[focus]
demote = ["Spotify", "Dropbox"]
nice_level = 10

Permissions

Closing apps, clearing your caches, and reading every statistic here need no privileges at all, and there is no daemon.

Focus mode needs exactly one grant, because of a macOS asymmetry: any user may raise a process's nice value, but only root may lower it again. Without help, deprioritising an app would be a one-way door — so Amphetamine refuses to demote anything until it can prove it can undo it.

amph setup asks for your password once and installs:

you ALL=(root) NOPASSWD: /usr/bin/renice 0 -p [0-9]*

The 0 is literal. The rule can only ever return a process to normal priority: it cannot deprioritise anything, cannot run any other command, and cannot open a shell. It is validated with visudo -c before installation — a malformed file in /etc/sudoers.d can lock you out of sudo entirely.

Inspect it with amph setup --print, remove it with amph setup --remove.

How it stays safe

A hard-coded denylist that config cannot override. The OS and your session (WindowServer, Finder, Dock, loginwindow), your work surface (Cursor, every common terminal), and everything holding mutable state (all VMs, Docker, Parallels, UTM) are refused even if you name them explicitly. Killing a VM abruptly can corrupt its disk image, so Amphetamine reports those and stops.

Apps are asked, never killed. Closing goes through NSRunningApplication::terminate, the same quit request as Cmd-Q. The app runs its normal shutdown and is free to put up an unsaved-work sheet. If it does not exit within the timeout it is simply left running — a stalled app is usually one asking you to save something. SIGKILL is never sent; --force exists and is never implied.

Caches must prove they are caches. ~/Library/Caches is not one kind of thing: alongside app caches it holds compiler and package-manager state that is ruinously expensive to rebuild. On the machine this was developed against, go-build alone was 91 GB and a dead-code analyser's cache was 39 GB. So the rule is a positive test rather than a denylist that could never keep pace: a bucket is eligible only if an application with that bundle identifier is actually installed, per LaunchServices. Everything unrecognised is left alone, which makes tools you install next year safe by default.

Then four more brakes: buckets holding live state (CloudKit, container manager) are permanently excluded; IDEs and browsers are skipped by default; a cache whose app is currently running is never touched, including its .ShipIt updater; and only files untouched — by both modification and access time — for min_age_days are eligible.

Deletion is narrow by construction. Only ~/Library/Caches and ~/Library/Logs, never configurable. Symlinks are never followed, and a symlinked bucket is skipped whole. Every path is re-canonicalised and re-vetted against its root immediately before removal, so nothing can escape through a link swapped in mid-run. There is no recursive delete anywhere: files are removed individually and directories only via remove_dir, which fails unless already empty.

Nothing irreversible. Focus mode verifies its restore path before touching a process, only demotes processes already at nice 0 (so restoring to 0 is exact rather than a guess), and restores on exit. If a session is killed outright, amph restore is the safety net.

Tests

cargo test

44 tests, run against the live machine rather than mocks. The ones that matter assert refusals: that the built-in denylist beats an explicit close request, that a symlink cannot lead a delete outside its root, that build-tool caches are never touched, that nothing is demoted without a proven way back, and that lowering a nice value without privileges still fails — the last one is a canary; if it ever passes, the sudoers grant is no longer needed.