A shell function that runs your update commands at most once every N days, triggered when you open a new terminal. Keeps development tools current without you remembering to do it.
- β° Time-based execution: Only runs updates after a specified number of days
- π§ Configurable commands: Define your own list of update commands
- π± Git repositories too: Point it at a local clone and it fast-forwards it β only when that needs no merge
- π Timestamp tracking: Remembers when updates were last run
- β‘ Command validation: Skips an entry when its program isn't installed
- π Concurrency safe: Opening ten tabs at once still runs the updates once
- π Cheap startup: The "nothing to do" path uses shell builtins only β no subprocesses
- π Status checking: View when updates last ran and when they're next due
- π Manual reset: Force updates to run on next terminal start
bash4.2+ orzsh(both are sourced the same way)- Works on macOS's stock
bash3.2 too β it just falls back to callingdate git1.8.5+ only if you list git repositories to update
-
Download the script:
mkdir -p ~/Scripts/auto_update_programs curl -o ~/Scripts/auto_update_programs/auto_update_programs.sh \ https://raw.githubusercontent.com/Code-Samples-Galore/Terminal-Auto-Update-Programs/main/auto_update_programs.sh
-
Source it in your shell config:
Add to your
~/.zshrcor~/.bashrc:source ~/Scripts/auto_update_programs/auto_update_programs.sh auto_update_check 7 "brew update && brew upgrade" "npm update -g"
The file is meant to be sourced, not executed β running it directly just defines the functions in a shell that immediately exits. No
chmod +xneeded.
Add this to your ~/.zshrc or ~/.bashrc to run updates every 7 days:
auto_update_check 7 "brew update && brew upgrade" "npm update -g" "pip install --upgrade pip"For better organization, use an array:
# Define your update commands
MY_UPDATE_COMMANDS=(
"conda update -n base -c defaults conda -y"
"brew update && brew upgrade"
"npm update -g"
"pip install --upgrade pip"
"rustup update"
"gem update --system"
)
# Run every 3 days
auto_update_check 3 "${MY_UPDATE_COMMANDS[@]}"Any entry that is a path to a directory is treated as a local git repository instead of a command, and is pulled on the same schedule:
auto_update_check 7 "brew update && brew upgrade" ~/Projects/dotfiles "$HOME/src/notes"Quoted ~ paths work too β "~/Projects/dotfiles" is expanded before the
directory is looked up.
Repositories are only ever fast-forwarded. If your branch can't simply be moved forward onto its upstream, the repository is reported and left exactly as it was β nothing is merged, rebased, stashed, or committed on your behalf:
| Situation | What happens |
|---|---|
| Behind upstream, nothing local | β Fast-forwarded, and the number of new commits is printed |
| Already up to date | β Reported as current |
| Uncommitted changes to tracked files | βοΈ Skipped β commit or stash them yourself |
| Local commits, or diverged from upstream | βοΈ Skipped β a merge is yours to make |
| Detached HEAD, or a branch with no upstream | βοΈ Skipped |
| Merge, rebase or cherry-pick in progress | βοΈ Skipped |
| Not a git repository, or a bare one | βοΈ Skipped |
| Fetch failed (offline, no credentials) | β Reported as a failure |
A path pointing inside a repository works β the repository containing it is the one that gets updated, and the message names its top-level directory.
auto_update_check [days] [commands-or-repo-paths...]β Check the interval and run updates if it elapsedauto_update_statusβ Show when updates last ran and when they're next dueauto_update_resetβ Clear state so the next terminal start updates
# Check every 5 days with specific commands
auto_update_check 5 "brew update && brew upgrade" "npm update -g"
# Mix commands and git repositories freely
auto_update_check 5 "brew update && brew upgrade" ~/Projects/dotfiles
# Use default commands (conda, brew, npm) every 7 days
auto_update_check 7
# Check status
auto_update_status
# Force updates on next terminal start
auto_update_reset| Variable | Default | Purpose |
|---|---|---|
AUTO_UPDATE_DISABLE |
unset | Set to any non-empty value to skip the check entirely. Useful in CI, scripts, and remote sessions. |
AUTO_UPDATE_INTERVAL |
7 |
Interval auto_update_status assumes when no run has been recorded yet. |
AUTO_UPDATE_LOCK_TIMEOUT |
21600 (6h) |
Seconds before a lock left behind by an interrupted run is considered stale. |
| Path | Contents |
|---|---|
~/.auto_update_timestamp |
Unix epoch of the last run |
~/.auto_update_interval |
Interval the last run used, so status reports the real next run |
~/.auto_update_lock |
Lock directory held only while updates are running |
Deleting these resets the timer β auto_update_reset does exactly that.
If no commands are provided, these defaults are used:
conda update -n base -c defaults conda -ybrew update && brew upgradenpm update -g
You can customize the commands for your specific setup:
# For Python developers
PYTHON_UPDATES=(
"pip install --upgrade pip"
"conda update --all -y"
"pipx upgrade-all"
)
# For Node.js developers
NODE_UPDATES=(
"npm update -g"
"yarn global upgrade"
"pnpm update -g"
)
# For Rust developers
RUST_UPDATES=(
"rustup update"
"cargo install-update -a"
)
# Combine and run
auto_update_check 7 "${PYTHON_UPDATES[@]}" "${NODE_UPDATES[@]}" "${RUST_UPDATES[@]}"- When you start a terminal, the function compares now against the recorded timestamp. If the interval hasn't elapsed it returns immediately, without spawning a single subprocess.
- Otherwise it takes a lock, so several shells starting at once produce exactly one update run.
- It records the timestamp before running anything, claiming the interval. An update interrupted halfway through therefore won't restart on every subsequent terminal.
- Each entry that names an existing directory is handled as a git repository: it is fetched, and fast-forwarded only if that requires no merge.
- Every other entry is checked for its leading program and skipped if that program isn't installed.
- Remaining entries run in order via
eval; successes and failures are both reported. - The lock is released and a summary is printed.
- Updates block your prompt. The commands run synchronously during shell startup, so a large
brew upgradedelays the terminal you just opened. SetAUTO_UPDATE_DISABLE=1for shells where that's unacceptable. - Commands run through
eval. That's what makes"brew update && brew upgrade"work as one entry. Only put commands you trust in your shell config. - Only the leading program of an entry is checked. In
"brew update && npm -g update", a missingnpmisn't caught in advance β it surfaces as a normal command failure. - The timestamp is kept even if commands fail. A permanently broken entry would otherwise retry on every single terminal start. Use
auto_update_resetto retry sooner. - Repository entries hit the network. Each one runs a
git fetch, so a slow or unreachable remote delays your prompt just like a slow command does. - Credential prompts are disabled during the fetch. A repository whose credentials aren't cached (and an SSH key that isn't in an agent) fails quickly and is reported, rather than stopping your shell startup at a password prompt.
- Submodules are left alone. Only the repository you name is fast-forwarded.
- An entry is a repository if the directory exists. A path with a typo in it doesn't match any directory, so it falls through and is reported as missing β check the spelling if a repository is never picked up.
Updates not running?
- Check
auto_update_statusto see when they last ran and when they're next due - Use
auto_update_resetto force them to run - Confirm
AUTO_UPDATE_DISABLEisn't set
Command not found errors?
- The script skips entries whose leading program isn't installed
- Make sure your PATH is set before
auto_update_checkruns in your shell config
auto_update_status says the state file is corrupt?
- Run
auto_update_resetto clear it
A git repository is never updated?
- The run prints a reason for every repository it leaves alone β uncommitted changes, local commits, a detached HEAD or a missing upstream are the common ones
git -C /path/to/repo status -sbshows the same state the script is reacting to- If the message says "no such file or directory", the path in your shell config doesn't exist
Status says an update is in progress but nothing is happening?
- An interrupted run can leave
~/.auto_update_lockbehind. It's reclaimed automatically afterAUTO_UPDATE_LOCK_TIMEOUT(6h by default), or immediately viaauto_update_reset.
Want to test without waiting?
- Use
auto_update_resetthen restart your terminal - Or call
auto_update_check 0 "your commands"to bypass the time check
MIT License