A reminder of how to set up my dotfiles on a new machine.
Prerequisite on Ubuntu (e.g. a fresh Docker container) — the Homebrew installer needs curl and git:
sudo apt install curl gitFollow the instructions at https://brew.sh/:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"After installing, if you want to use brew one time only (without polluting your rc files), load it into the current shell (Linux):
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv bash)"On macOS, use
eval "$(/opt/homebrew/bin/brew shellenv bash)"instead.
Alternative: Nix Package Manager
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install)or,
sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --no-daemonpersonal choice, especially inside a container
. $HOME/.nix-profile/etc/profile.d/nix.shNote: nix-command and flakes are automatically enabled with chezmoi apply.
brew install zsh chezmoi nushellNote: the package is
nushell, notnu.
Alternative: zsh via system package manager
sudo pacman -S zshsudo dnf install zshsudo apt install zshAlternative: chezmoi via Nix
nix --extra-experimental-features nix-command --extra-experimental-features flakes profile install nixpkgs#chezmoiThe personal tool installers also require Git, uv, and just on PATH:
brew install git uv justThen apply:
# github handle after --apply
chezmoi init --apply vimkimchezmoi init --apply only clones when the source directory does not exist
yet; it does not pull remote updates. To sync a patch made on another machine,
either pull-and-apply in one step:
chezmoi updateor, when the machine might have local edits worth checking first:
chezmoi git pull -- --autostash --rebase # just pull the source repo
chezmoi diff # see what apply would change
chezmoi apply # apply when happyNone of these are hard requirements — config.nu quietly skips every tool that is not installed. Recommended for the interactive experience (prompt, history, completions; without carapace, Tab-completion for external commands does not work):
brew install starship atuin carapaceOptional tools
Optional tools used by aliases and helper commands (e.g. eza for l,
fzf/fd/ripgrep for the pickers, lazygit for lz, delta as the git/lazygit
pager):
brew install eza fzf fd ripgrep lazygit deltaMisc dependencies:
brew install mise direnv zoxide fastfetch diffnav ghThe terminal configuration uses Maple Mono. On Fedora, follow Installing Maple Mono Nerd Font.
Interactive Bash and Zsh load ~/.config/my-scripts/shell/productivity.sh.
These everyday shortcuts also exist in Nushell:
| Command | Behavior |
|---|---|
c, i |
Pick a directory with dir-picker, then list its contents. |
c DIR |
Change directly to DIR, then list its contents. |
c [DIR] --query TEXT |
Browse DIR (current directory by default) with an initial name query. |
l, sl |
Detailed eza grid with hidden entries, icons, Git state, and modification-time sorting. |
ll |
Detailed eza rows with owners and permissions. |
h, up, back |
Go to the parent or previous directory and list it. |
mc DIR |
Create a directory, including parents, and enter it. |
gr, gcd, pr, pcd |
Go to the Git repository root and list it. |
cz |
Open zoxide's interactive directory picker (zi). |
ch |
Pick a previously visited directory from the shared directory history. |
ch --forget, ch --delete, ch --prune |
Forget selected entries, confirm moving selected directories to trash, or prune missing paths. |
chf, chd, chp, vh |
Shortcuts for forget, trash, prune, and viewing history. |
n, na |
Pick a Just recipe from ./justfile or ~/.config/my-scripts/justfile. |
nr, nc |
Pick from $MY_CUBRID/remote-nu.just or $MY_CUBRID/cubrid-justfiles/justfile (MY_CUBRID defaults to ~/my-cubrid). |
fp [QUERY] |
Print a selected file's path using file-picker.sh. |
cf [QUERY] |
Enter the directory containing a selected file. |
vc [QUERY] |
Edit a selected file from the current directory. |
y |
Run Yazi and enter its exit directory. |
fzfm, fda, rgall |
Fuzzy finder, fd including hidden/ignored files, and ripgrep including hidden/ignored files. |
conf, confd, mycub, nvimh |
Jump to configuration, CUBRID, or Neovim directories. |
lz, lj, lzd, v, py, sctl |
Lazygit, lazyjj, lazydocker, Neovim, Python 3, and systemctl. |
lzds |
sudo-path lazydocker: run lazydocker with sudo. |
lzp |
lazydocker-with-podman: use the system Podman socket without sudo. |
lzps |
sudo-path lazydocker-with-podman: use the system Podman socket with sudo. |
ports, ports-tcp, ports-all |
Existing port listing helper, its TCP view, or somo. |
cd retains its existing change-and-list behavior; bare cd goes home.
l and ll fall back to system ls when eza is unavailable. Pickers require
the existing managed dir-picker / file-picker.sh commands and their
dependencies (Python 3, fzf, and, for files, fd/eza). Cancelling a picker leaves
the current directory unchanged. Other shortcuts require their named tools;
they do not install them during shell startup.
sudo-path COMMAND [ARG ...] resolves an external executable from the caller's
PATH, then passes its absolute path and the caller's PATH to sudo. Preserving
PATH lets wrapper scripts find their child commands after sudo starts them.
It also accepts explicit executable paths and forwards all arguments and the exit
status. It requires only sh and sudo, with no Nix or Homebrew dependency.
lazydocker-with-podman sets DOCKER_HOST=unix:///run/podman/podman.sock and
executes lazydocker. It does not request sudo itself. The lzd, lzds, lzp,
and lzps aliases are available in Bash, Zsh, and Nushell; their s suffix
selects sudo. The retired lzps executable is removed by chezmoi.
The Just pickers prepare an editable command: Zsh preloads the next prompt;
Bash opens a Run: Readline prompt where Enter runs the command and records it
in history. Noninteractive Bash prints the prepared command. They use the existing
just-pick-and-print.nu helper and require Nushell, Just, and fzf.
Directory history uses the shared Python dir-history command and a SQLite
database under ${XDG_STATE_HOME:-$HOME/.local/state}/dir-history/. Bash records
directory changes before each prompt; Zsh and Nushell record them on directory
changes. The existing JSON history is imported once and left unchanged.
See directory history for commands, concurrency,
migration, and reloading existing sessions.
ls-by-size, ls-by-name, and ls-by-time provide detailed eza listings sorted
largest first, alphabetically, and newest first. ls-by-extension, ls-tree,
ls-dirs, and ls-files add extension sorting, a two-level tree, and filtered
views. All include hidden entries and accept paths and eza options.
In a terminal, long output opens in less; short output stays visible. Use
--no-pager to print directly and --reverse to reverse the default order.
Pipes and redirected output bypass paging. These Python 3 scripts work in
Nushell, Bash, and Zsh; see usage and verification.
Run handoff-dir to create .handoff/ in the current directory and write *
to .handoff/.gitignore, ignoring everything inside. The Python 3 command lives
in ~/.config/my-scripts/bin; running it again preserves the directory's other
contents and rewrites its .gitignore.
It reports Created: .handoff/ or Already exists: .handoff/ after completing
successfully.
chezmoi-apply-unmodified (in ~/.config/my-scripts/bin) applies managed files
and symlinks while reporting locally changed targets as successful skips.
Run it without arguments to use chezmoi's configured source checkout; --source
selects a different checkout. See usage and verification.
Run gh-html /path/to/repository/report.html to print a rendered githack URL.
It infers the GitHub remote, repository path, and current commit from a local
file, or converts a GitHub blob/raw URL directly. --cdn selects the CDN host.
The file must already be committed and pushed to a public GitHub repository.
The Python 3 command lives in ~/.config/my-scripts/bin; see
usage and verification.
Run html-serve from any directory to serve it until Ctrl+C. It shows a directory
listing with clickable Title, Created, and Modified column headers. Add --file review.html to
link directly to an HTML file, or --port 8080 to choose a port.
The standalone project in ~/gh/html-serve installs the command with
just sync. It prefers memorable four-digit free ports and prints Tailscale/VPN
and LAN links for opening from your PC. Bootstrap and daily-update maintain
the installation; old script paths forward to it. See usage and verification.
Chezmoi installs Dirpick from ~/gh/dirpick
into ~/.local/bin, which Bash, Zsh, and Nushell already include on PATH.
Git, Just, and a current Rust/Cargo toolchain are prerequisites. The first
chezmoi init --apply installs it once. daily-update maintains the installation
through the checkout's just sync recipe.
Run dirpick to open the terminal workbench. In Nushell, run dp to also change
the shell directory with Ctrl-G. The default scope is Project root. F7 switches
between Project root and the launch CWD. See installation and updates.
daily-update (in ~/.config/my-scripts/bin) updates dotfiles, personal tools
including Dirpick, Codex, Claude Code, and globally installed agent skills:
daily-update # run everything, report stale skills, mark today done
daily-update --status # show current versions and whether today is done
daily-update --stale # list stale skills, changing nothing
daily-update --prune # delete the stale skills that --stale listed
daily-update --reset # clear today's mark so the reminder comes backBoth config.nu and .zshrc call daily-update --remind at the end of
interactive startup. That prints a pending block on every new shell and keeps
printing until daily-update actually runs; the run writes today's date to
~/.cache/daily-update.stamp, which silences the reminder for the rest of the
calendar day. Seeing the reminder never dismisses it, which is the difference
from the PR digest above.
The stamp records that a run happened, not that every step succeeded. A failed step is listed at the end of the run and left for a deliberate re-run, so a network blip cannot trap the reminder in a loop.
Skills come from the skills CLI and are tracked in ~/.agents/.skill-lock.json.
Because ~/.claude/skills/* symlinks into ~/.agents/skills, the single
skills update --global covers both Claude Code and Codex.
skills update never removes anything, and the CLI has no prune command, so a
skill deleted from its source repository stays installed and active forever.
Each run therefore reports what has gone stale and points at --prune. The
startup reminder then names those skills, reading them from cache so it stays
offline and fast; past eight names it summarises the remainder as a count.
A skill is stale only if the lock file claims it and one of these holds:
- it no longer exists in its source repository,
- its directory under
~/.agents/skillsis gone, or - its
~/.claude/skillssymlink is broken.
Everything else is left alone. This matters because the lock file records only
GitHub-sourced installs: the whole my-cubrid-skills collection is installed
from a local path and never appears in it, so treating the lock as an allowlist
would delete every personal skill. Names present in ~/gh/my-cubrid-skills are
additionally protected even if a source repo later publishes the same name.
Two failure modes are guarded explicitly. A gh failure is never read as "the
repository is empty", and because gh api prints its 404 body to stdout, the
upstream list is accepted only when the command succeeds and every line looks
like a bare directory name. --prune moves what it removes into
~/.cache/daily-update/trash/<timestamp>/, with a copy of the lock file, rather
than deleting outright.
Upgrades always resolve ~/.local/bin first rather than trusting the calling
shell's PATH, because the two disagree on this host: zsh finds a mise-managed
npm codex and a stale root-owned /usr/bin/claude (npm global, 2.0.25) that
the nushell login shell never sees. Without the pin, running daily-update
from zsh would upgrade a different install than the one you actually use.
The display-environment, XKB layout, and dead_hamza packaging diagnosis is
recorded in WezTerm on Fedora KDE through Herdr,
including verification and rollback commands.
Herdr's tab agent markers, workspace tab inventory, and updater controls are documented in Herdr tab agent indicators.
Alt+C creates a Space immediately below the current one; Alt+Shift+C opens the
numbered Space reorder popup. Both use
vimkim/herdr-next-to-current,
which chezmoi clones into ~/gh/herdr-next-to-current and installs with
just sync. daily-update refreshes that checkout alongside my-git-utils.
These shortcuts require Linux/WSL, Python 3.11+ with curses, and a Herdr
0.9.3 server using protocol 22. There are no third-party Python runtime
dependencies. uv selects a compatible Python during installation; the installer
checks that installed interpreter, rather than the system python3.
Missing Git, uv, or just, failed installation, or an unusable Python runtime
produces an error and leaves installation eligible to retry with chezmoi apply.
Missing Herdr, an unreadable version, or a different installed Herdr version
produces a warning without blocking the other dotfiles. The helper also checks
the actual server version and protocol when a shortcut runs. The installer runs
once per script revision; it does not check dependencies at every shell startup.
The KDE Wayland configuration includes a standalone Colemak layout with programming symbols on the unshifted number row. Follow Installing the Colemak Programmer Layout to validate and apply it without shadowing the system US XKB symbols file.
Optional Step: Install oh-my-zsh
I now use nushell instead, by the way.
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
mv .zshrc.pre-oh-my-zsh .zshrcOptional Step: Other nix packages
Only if you installed Nix. This might take some time and disk spaces.
my-nix-install.shcd $HOME/.config
git clone https://github.com/vimkim/nvimsudo dnf install xauth
sudo apt install xauth
sudo pacman -S xorg-xauth# /etc/ssh/sshd_config
X11Forwarding yes
X11DisplayOffset 10
X11UseLocalhost yesSee daily-update for refresh policy, partial results, third-party boundaries, and isolated verification.