Keep all your local clones up to date in the background, so you never cd into a stale checkout. Uses gitup + a launchd agent that fires every 30 minutes.
gitup is "safe by default" — it skips dirty working trees, diverged branches, and detached HEADs instead of clobbering them. Combine it with the discipline of only editing inside a git worktree and your main checkouts stay clean mirrors of origin.
You need uv (one-liner installer):
curl -LsSf https://astral.sh/uv/install.sh | shuv tool install gitupThis drops the gitup binary in ~/.local/bin (make sure that's on your PATH — uv tool install will warn you and show the line to add to .zshrc if it isn't).
gitup --add ~/Developer/*Glob expands to every top-level directory; gitup only bookmarks the ones that are actually git repos. Re-run any time you clone new stuff (or let the LaunchAgent below do it for you — it re-adds on every run).
Verify:
gitup --listDry-run pull once to confirm it works:
gitupTwo gotchas this avoids:
launchddoes not expand$HOME(or anything else) inside plist strings — so we interpolate at heredoc write-time.zsh -cruns as a non-interactive shell, which does not source.zshrc.uv tool installadds its PATH line to.zshrc, so launchd wouldn't findgitupon its own. We resolve the absolute path with$(command -v gitup)at write-time and bake it in.
mkdir -p ~/Library/LaunchAgents ~/Library/Logs
GITUP=$(command -v gitup)
test -x "$GITUP" || { echo "gitup not on PATH — fix that first"; exit 1; }
cat > ~/Library/LaunchAgents/com.user.gitup.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.gitup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>-c</string>
<string>$GITUP --add \$HOME/Developer/* 2>/dev/null; $GITUP</string>
</array>
<key>StartInterval</key>
<integer>1800</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>$HOME/Library/Logs/gitup.log</string>
<key>StandardErrorPath</key>
<string>$HOME/Library/Logs/gitup.log</string>
</dict>
</plist>
EOFLoad it:
launchctl unload ~/Library/LaunchAgents/com.user.gitup.plist 2>/dev/null
launchctl load ~/Library/LaunchAgents/com.user.gitup.plistRunAtLoad triggers an immediate first run; after that, every 30 minutes (1800 seconds). Tail the log to confirm:
tail -f ~/Library/Logs/gitup.log# pause auto-pull
launchctl unload ~/Library/LaunchAgents/com.user.gitup.plist
# resume
launchctl load ~/Library/LaunchAgents/com.user.gitup.plist
# trigger one run on demand
launchctl kickstart -k gui/$(id -u)/com.user.gitup
# is it loaded?
launchctl list | grep gituplaunchctl unload ~/Library/LaunchAgents/com.user.gitup.plist
rm ~/Library/LaunchAgents/com.user.gitup.plist
uv tool uninstall gitup
rm -rf ~/.gitup # removes bookmark listgitup won't clobber a dirty tree — it skips and warns. But that means if you're sitting on uncommitted edits in ~/Developer/foo, that repo silently stops auto-updating until you clean up. Drift sneaks in.
The fix: never edit the main checkout directly. Use git worktree add ../foo-feature -b my-feature (or your editor's worktree integration) so the primary checkout always tracks origin and your edits live in a sibling directory. Agents like Claude Code can be configured to enforce this — but the rule applies whether or not you use one.