upkeep
Keep your tools up to date. up runs the update commands you list in a TOML file: the ones you pick, all of them in parallel, or, from an hourly job, the ones you've opted in that are due.
"Update everything" scripts tend to fail quietly when nobody is watching: a cask wants sudo, a git remote wants an SSH key touch, a CLI isn't logged in, two tools run brew at once. upkeep is built around that. Nothing runs unattended unless you mark it auto, tools that share a package manager take turns, every run is logged, and failures leave a one-line notice for your next shell. topgrade is the big prior art and knows hundreds of tools. upkeep knows eleven presets and runs whatever else you tell it to: small, and config first.
Install
brew install wmxscott/tap/upkeep
# or
uv tool install git+https://github.com/wmxscott/upkeep
# or
pipx install git+https://github.com/wmxscott/upkeepNeeds Python 3.11 or later, on macOS or Linux. Both up and upkeep are installed. Homebrew core has an unrelated up, so the formula conflicts with it; every command here works as upkeep too.
Quick start
up init # write ~/.config/upkeep/config.toml with the presets it finds
up brew mise # run two tools
up # pick from a list, most used first
up --all # run every tool, in parallel
up schedule install # an hourly job that runs the auto tools when they're dueSet auto = true on the tools you trust to run unattended, then check the setup with up doctor.
Configuration
~/.config/upkeep/config.toml, or $XDGCONFIGHOME/upkeep/config.toml. Set UPKEEPCONFIG to use another file.
[schedule]
at = "08:00" # daily and weekly tools become due at or after this local time
shell = "zsh -lic" # how commands and the scheduled job start; default "$SHELL -lc"
[logs]
keep_days = 30
[notify]
on = "failure" # desktop notification after a scheduled run: failure, always, never
[tools.brew]
preset = "brew" # a built-in recipe; any key below overrides it[tools.claude] run = "claude update" version = "claude --version" auto = true
[tools.codex] run = "brew upgrade codex" lock = "brew" # never at the same time as the brew tool auto = true
[tools.dotfiles] run = "cd ~/dotfiles && git pull" # an SSH remote: better kept manual
| Tool key | Default | |
|---|---|---|
| `run` | | The command, run through `shell`. Required unless a preset gives one |
| `preset` | | A built-in recipe, below |
| `auto` | `false` | Run by `up --auto`, which is what the schedule runs |
| `lock` | | Tools with the same lock never run at once, in this or any other `up` process |
| `requires` | | An executable, or a list. When one isn't on `PATH`, the tool is skipped rather than failed |
| `version` | | A command that prints the version on its first line. Recorded before and after; `up status` shows the version number, or `1.0 → 1.1` when a run changed it |
| `check` | | What `up check` runs. It should print one line per outdated item and nothing otherwise |
| `interval` | `"daily"` | `"daily"`, `"weekly"`, `"<n>h"` or `"<n>d"`. Only `--auto` uses it |
| `timeout` | `"1h"` | `"90s"`, `"30m"`, `"2h"`, or `0` for none. The whole process group is killed |Commands run through shell, with stdin closed and in their own process group. The default is your $SHELL -lc, so they see your login PATH, when that shell speaks POSIX sh (sh, bash, zsh, dash, ksh); otherwise it's /bin/sh -lc. A tool named like a command (status, log, …) runs with up run <name.
Unknown keys and bad values are errors in up doctor. Everywhere else they're warnings, and the default is used.