Troubleshooting
Quick reference for when things go wrong. Check here before digging into scripts.
Tool not found after bootstrap
echo "$_PLAT" "$_LOCAL_PLAT" # capability + install root
ls "$_LOCAL_PLAT/bin/" # chezmoi, uv, claude should be here
ls "$_LOCAL_PLAT/cargo/bin/" # fd, sd, zoxide, etc.
which fd # should point under $_LOCAL_PLAT
$_LOCAL_PLAT is $HOME/.local by default (flat layout) or $HOME/.local/$_PLAT when PLAT isolation is enabled. If $_PLAT or $_LOCAL_PLAT is empty, .zprofile wasn’t sourced. Open a new login shell (zsh -l) or source it:
source ~/.zprofile
nvm or node not available in a script
nvm.sh is lazy-loaded in interactive shells only. Non-interactive shells get node/npm via the PATH entry .zprofile/.bash_profile adds from the highest installed version. If node is missing in a script, either:
# Option 1: source profile at the top of your script (zsh)
source ~/.zprofile
# Option 1b: source profile at the top of your script (bash)
source ~/.bash_profile
# Option 2: use the full path
NODE="$NVM_DIR/versions/node/$(ls $NVM_DIR/versions/node | sort -V | tail -1)/bin/node"
chezmoi keeps prompting for name/email
The cached values live in ~/.config/chezmoi/chezmoi.toml. To reset:
chezmoi init --data=false
To pre-seed without prompting:
DF_NAME="Your Name" DF_EMAIL="you@example.com" chezmoi init
chezmoi diff shows unexpected changes
Another program modified a managed file. Common culprits:
uvauto-adds source lines to.zshrc/.bashrcfor itsbin/envfiles- Claude Code updates
~/.claude/settings.jsonwhen plugins are installed - Other tools may modify shell configs without asking
Options:
chezmoi diff # see what changed
chezmoi apply --force # overwrite with repo version (safe for shell configs)
chezmoi add ~/.claude/settings.json # pull the live version into the repo (for config files)
For shell configs (.zshrc, .zprofile, .bash_profile), always use chezmoi apply --force to restore the clean template. These files should never be manually edited.
PATH order is wrong — wrong binary is resolving
Expected priority (highest to lowest). $_LOCAL_PLAT collapses to $HOME/.local in flat-mode (default).
$_LOCAL_PLAT/cargo/bin Rust tools (fd, sd, zoxide, bat, rg, etc.)
$_LOCAL_PLAT/nvm/.../bin Node.js (highest installed version)
$_LOCAL_PLAT/bin chezmoi, uv, claude, codex, uv-tool entrypoints
~/.local/bin arch-neutral scripts (collapses to $_LOCAL_PLAT/bin in flat mode — deduped via typeset -U)
/opt/homebrew/bin Homebrew (macOS) — also where rustup lives
/opt/homebrew/sbin Homebrew sbin
/usr/bin system
Diagnose with:
which <tool> # where it's resolving from
type -a <tool> # all locations on PATH
echo $PATH | tr ':' '\n' # full PATH in order
If a Homebrew tool is shadowing a cargo tool, check packages/cargo.txt and packages/Brewfile for duplicates — remove the one you don’t want.
The other classic shadowing footgun: legacy binaries at ~/.local/bin/<tool> from before a layout migration. The [[ -x "$ARCH_BIN/<tool>" ]] install checks in current scripts catch most of these, but if <tool> --version shows an unexpectedly old version, check ls ~/.local/bin/<tool>* for backups (*.preplat-bak.* or stale binaries) and delete them.
Cloudflare Pages build failing
Check the build log via the API:
ACCOUNT="YOUR_CLOUDFLARE_ACCOUNT_ID"
TOKEN="..."
# List recent deployments
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/pages/projects/dotfiles/deployments" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool | grep -E '"id"|"status"'
# Get logs for a specific deployment
DEPLOY_ID="..."
curl -s "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT/pages/projects/dotfiles/deployments/$DEPLOY_ID/history/logs" \
-H "Authorization: Bearer $TOKEN" | python3 -c "
import sys, json
for e in json.load(sys.stdin)['result']['data']: print(e['line'])
"
Common causes:
cargo-binstall: command not found—/opt/buildhome/.cargo/binnot on PATH; checkinfra/cloudflare/build.shmdbook: command not found— binstall failed; check network or fall back tocargo install mdbook --locked- Build output not found — confirm
destination_dir = "docs/book"ininfra/cloudflare/main.tf
Two machines fighting over dotfiles on a shared home
This happens when a template renders differently on each machine (e.g. using {{ .chezmoi.arch }}). The rule: templates must be arch-neutral. Arch-specific logic belongs in shell runtime code, not templates.
Check which template is causing the conflict:
chezmoi diff # shows what chezmoi wants to change vs what's on disk
The fix is almost always to replace a template variable with a shell runtime expression. See Managing dotfiles → Shared home safety.
Duplicate PLAT paths in PATH (both v3 and v4 showing up)
Only relevant with DF_USE_PLAT=1. Fixed in current versions — .zprofile/.bash_profile resolve ~/.local symlinks before setting _LOCAL_PLAT so PATH entries use the same physical path.
If you upgraded from before that fix:
chezmoi apply ~/.zprofile ~/.bash_profile
exec zsh -l # or: exec bash -l
echo "$PATH" | tr ':' '\n' | grep plat # all entries should share the same PLAT prefix
In flat mode (DF_USE_PLAT=0, the default), this failure mode doesn’t apply — there’s no $PLAT segment in $_LOCAL_PLAT.
Lost shell history
Zsh history lives at ~/.zsh_history (the conventional default; survives any ~/.local cleanup). Bash history at ~/.bash_history. The bash sidecar command log (richer: timestamps, exit codes, cwd) at ~/.bash_log — search via bash_log_search <pattern>.
If you have history under the old location (~/.local/state/{zsh,bash}/), one-time migrate:
[ -f ~/.local/state/zsh/history ] && mv ~/.local/state/zsh/history ~/.zsh_history
[ -f ~/.local/state/bash/history ] && mv ~/.local/state/bash/history ~/.bash_history
[ -f ~/.local/state/bash/log ] && mv ~/.local/state/bash/log ~/.bash_log
Migrating off PLAT isolation
If you set up with DF_USE_PLAT=1 and want to switch to flat (or vice-versa), the layout in ~/.local/ is stable as long as one mode is active — but switching strands GBs in the unused tree. Decommission tool:
# After setting DF_USE_PLAT=0 (or removing use_plat=true from chezmoi data):
bash ~/dotfiles/install/plat-decommission.sh
Refuses to run if DF_USE_PLAT=1 is currently set (won’t nuke the active install). See PLAT isolation for the full migration story.
Brew zsh tab completion leaves remnant characters (Linux)
Symptom: after pressing Tab, stale characters remain on the line instead of being erased.
Root cause chain:
- Brew zsh’s RUNPATH loads Homebrew’s own glibc (
brew/opt/glibc/lib/libc.so.6) - Homebrew’s glibc ships no
lib/locale/data →setlocale()silently falls back toC/ASCII - In the C locale,
wcwidth()returns byte counts instead of display columns - Every cursor-position calculation in ZLE/completion is off → artifacts
Confirm by checking the codeset inside brew zsh:
zsh --no-rcs -c 'zmodload zsh/langinfo; echo $langinfo[CODESET]'
# broken: ANSI_X3.4-1968
# working: UTF-8
Fix: linux-packages.sh generates en_US.UTF-8 locale data for brew’s glibc into
$LOCAL_PLAT/locale/ using brew’s own localedef. The shell profiles export LOCPATH
pointing there so brew zsh picks it up at startup.
If you installed before this fix:
# Regenerate locale data
bash ~/dotfiles/install/linux-packages.sh
# Apply updated shell profiles (adds LOCPATH export)
chezmoi apply ~/.zprofile ~/.bash_profile
# Open a new login shell and verify
exec zsh -l
zsh --no-rcs -c 'zmodload zsh/langinfo; echo $langinfo[CODESET]' # UTF-8
Test suite: bash ~/dotfiles/tests/test-locale.sh
Python@3.14 build fails on Linux (uuid or test_datetime errors)
Python 3.14 from Homebrew has build issues on some Linux systems:
- UUID module detection failure - configure detects libuuid but the build fails
- test_datetime hangs during PGO - Profile-guided optimization runs the test suite, but
test_datetimehangs on some CPUs (timezone-related)
Fix: Patches are applied automatically by install/patch-homebrew-python.sh during bootstrap. If you need to re-apply manually:
bash ~/dotfiles/install/patch-homebrew-python.sh
brew reinstall --build-from-source python@3.14
The patches:
- Set
py_cv_module__uuid=n/ato disable the uuid module - Patch Makefile’s
PROFILE_TASKto skiptest_datetimeduring PGO
Environment variables in .zprofile/.bash_profile prevent Homebrew from auto-updating and overwriting these patches:
HOMEBREW_NO_AUTO_UPDATE=1- prevents tap updatesHOMEBREW_NO_INSTALL_FROM_API=1- forces local formula usage
git push blocked by gitleaks (“secrets detected”)
A global pre-push hook scans the commits being pushed for secrets with gitleaks and refuses the push if it finds any. This is the safety net that keeps tokens and private keys out of remote history — see Authentication → File security.
How it’s wired:
brew "gitleaks"(inpackages/Brewfile) installs the scanner.- The hook lives at
home/dot_config/git/hooks/executable_pre-push, deployed by chezmoi to~/.config/git/hooks/pre-push. ~/.gitconfigsetscore.hooksPath = ~/.config/git/hooks, so it applies to every repo on the machine, not just dotfiles.- It scans only the commits being pushed (a new branch is scanned against
--remotes), not the full history, so it stays fast. - If gitleaks isn’t installed yet, the hook prints a warning and exits cleanly rather than blocking you.
When a push is blocked, the hook prints the exact --log-opts range it flagged.
Review the finding:
# Re-run the scan the hook ran (range is printed in the failure message)
gitleaks git --log-opts="<remote_sha>..<local_sha>"
# Or scan the entire repo history
gitleaks git --no-banner
If it’s a real secret: rotate it, then rewrite the offending commit(s) to remove
it before pushing (a --no-verify push would leak it to the remote). If it’s a
confirmed false positive, add a gitleaks allowlist entry rather than
disabling the hook.
Emergency bypass (use only when you’re certain there’s no secret):
git push --no-verify
Don’t disable the hook permanently — core.hooksPath is global precisely so the
protection can’t be forgotten on a per-repo basis.