This page is the front door to my dotfiles. It is written for one reader first: me, the next time I am sitting in front of a machine that does not know me yet. If you are someone else, welcome — take whatever is useful.
Everything I care about in a terminal is in that repo: zsh with Powerlevel10k, tmux on a Ctrl+a prefix, Neovim through LazyVim, Ghostty in Catppuccin Mocha, lazygit, and the settings for the AI agents I work with every day. None of it is copied into place. It is linked, so the repo stays the only copy, and a change on one machine is a git pull away from every other.
The one-liner
curl -fsSL https://install.kristoffer.dev/dotfiles | bash
It does five things, in order, and prints each as it goes:
- Installs git and GNU Stow if they are missing, with whatever package manager the machine has — Homebrew first, then apt, dnf or pacman. Nothing else, unless you ask.
- Clones the repo into
~/dotfiles. If it is already there, it fast-forwards it instead, and leaves it alone if it has uncommitted changes. - Moves anything in the way into
~/.dotfiles-backup/<timestamp>/. A fresh OS often writes a default config on first launch; Stow refuses to link over a real file. Nothing is deleted. - Hands over to the repo’s own
install.sh, which does the linking. The bootstrap only gets the machine to where the repo can run, so it rarely needs to change. - Checks what is still missing and says what to do about it.
Run it twice and nothing breaks: the second run pulls, finds nothing in the way, and relinks the same links.
Options
Flags go after bash -s --:
curl -fsSL https://install.kristoffer.dev/dotfiles | bash -s -- --dry-run --tools
| Flag | Does |
|---|---|
--dry-run | Prints every change instead of making it — including what would be moved aside |
--tools | Also installs zsh, tmux, Neovim, lazygit, Ghostty (macOS), Oh My Zsh and Powerlevel10k |
--no-deps | Installs nothing; fails if git or Stow is missing |
--dir DIR | Clones somewhere other than ~/dotfiles |
--branch B | Uses a branch other than main — handy for testing a change on a fresh VM |
Read it before you pipe it
Piping a URL into a shell is trusting whoever controls the URL. It is my domain, and I still would not do it blind on someone else’s. The script is here, under four hundred lines of commented bash. Everything runs inside a main function called on the last line, so a download cut off halfway runs nothing.
install.kristoffer.dev/dotfiles is a short name for https://kristoffer.dev/dotfiles/install.sh; both work.
A new machine, start to finish
This is the checklist. It is the reason this page exists.
macOS
xcode-select --install— gets you git.- Install Homebrew. The installer finds it and prefers it from then on.
- Run the one-liner with
--tools. On a Mac that includes Ghostty, as a cask. - Open Ghostty instead of Terminal.app. It ships with a Nerd Font built in, which is what makes the Powerlevel10k prompt render properly.
exec zsh. The first start runs the Powerlevel10k wizard — answer it once.nvim. The first launch installs every plugin. Give it a minute and do not panic at the wall of text.- Add an SSH key, then
gh auth login.
Ubuntu, Debian and WSL
sudo apt install -y curl- Install Homebrew for Linux first — it is worth it. apt’s Neovim lags years behind what LazyVim needs, and apt has no lazygit on older releases. With Homebrew present, the installer uses it for everything.
- Run the one-liner with
--tools. chsh -s "$(command -v zsh)", then log out and in again. The installer reminds you if the login shell is not zsh yet.- Same as macOS from step 5: the wizard, Neovim’s first launch, SSH keys.
Ghostty on Linux is a separate install; the terminal config is linked either way and waits for it.
Machine-specific settings
Anything that belongs to one machine — a work proxy, a token, a PATH entry for something only installed here — goes in ~/.config/zsh/.zshrc.local. The .zshrc sources it if it exists, and git ignores it, so it never leaves the machine.
What’s inside
Each folder in the repo becomes one symlink:
| Package | Linked to | What it brings |
|---|---|---|
zsh | ~/.config/zsh | Oh My Zsh + Powerlevel10k, aliases, tmux helpers, CLI completions |
tmux | ~/.config/tmux | Ctrl+a prefix, vim-style panes, mouse on, OSC 52 clipboard |
nvim | ~/.config/nvim | LazyVim with .NET, Go, Vue, Tailwind, DAP and Claude Code extras |
ghostty | ~/.config/ghostty | Catppuccin Mocha, clipboard read/write allowed |
lazygit | ~/.config/lazygit | Nerd Font v3 icons |
opencode | ~/.config/opencode | Nine agents (companion, reviewer, architect …) and skills |
claude | ~/.claude/settings.json | Claude Code settings and hooks |
The trick that makes zsh fit this layout is one line the installer adds to ~/.zshenv:
export ZDOTDIR="$HOME/.config/zsh"
zsh reads ~/.zshenv before anything else, and ZDOTDIR tells it to look for the rest of its config in ~/.config/zsh/ — which is a link into the repo. The home directory stays clean: one small file, not a .zshrc, .zprofile and .p10k.zsh side by side.
If symlinks are new to you, I wrote about them in Link up your life. Stow is just a tool that makes a whole tree of them in one command, and removes them again with -D.
Daily use
Aliases
The ones I type without thinking:
| Alias | Runs | Why |
|---|---|---|
n | nvim . | Open the editor on the current directory |
ll | ls -al | Long listing, dotfiles included |
gss | git status -s | Short status, the one I type most |
gaa | git add . | Stage everything |
gcm | git commit -m | Commit with a message |
gpsh | git push | Push |
c | claude --allow-dangerously-skip-permissions | Claude Code, no prompts — only in a sandbox |
cc | claude | Claude Code, with prompts |
oc | opencode | OpenCode |
dtable | docker ps --format table … | Containers as a readable table |
dstart | docker compose up -d && docker compose logs -f | Start the stack and tail it |
tmx | tmx <session> | Attach to a tmux session, or offer to create it |
tmux
The prefix is Ctrl+a because Ctrl+b is a reach. Splits are the characters that look like the split:
| Keys | Does |
|---|---|
| Ctrl+a | Prefix (instead of Ctrl+b) |
| Ctrl+a | | Split left/right |
| Ctrl+a - | Split top/bottom |
| Ctrl+a h/j/k/l | Move between panes |
| Ctrl+a [ | Copy mode, vi keys |
| Ctrl+a r | Reload the config |
Clipboard goes over OSC 52, so a yank in tmux or Neovim lands on the laptop’s clipboard even when the shell is on a server over SSH. Ghostty is set to allow it.
Changing things
Because everything is a link, there is no “apply” step. Edit ~/.config/tmux/tmux.conf and you have edited the file in the repo:
nvim ~/.config/zsh/.zshrc # = ~/dotfiles/.config/zsh/.zshrc
cd ~/dotfiles && gss # see what changed
gaa && gcm "zsh: add alias" && gpsh
On every other machine, the one-liner again: it pulls and relinks.
To bring a new tool under the repo, move its config folder into ~/dotfiles/.config/ and run ~/dotfiles/install.sh. Stow links it back where it came from.
Try it in the browser
This site has a terminal — press ` anywhere, or the >_ button in the corner. It knows exactly one script, and it is this one.
The button at the top types the one-liner into it: curl -fsSL https://install.kristoffer.dev/dotfiles | bash. It runs the same steps as the real installer, with the same output, and then the terminal becomes mine. The colours turn to Catppuccin Mocha. The prompt turns into Powerlevel10k segments. The title bar says Ghostty. And the aliases work — or at least, do the most a browser tab can:
aliaslists them allll ~/.configshows the links Stow madecat ~/.config/zsh/.zshrcprints what is in themnopens Neovim, as far as the splash screentmuxandneofetchdo what you would expectdotfiles difflines up before and after
It stays installed as you move around the site, the way a real machine keeps it. dotfiles uninstall — or stow -D, if you prefer the real verb — puts the stock terminal back.
It is a toy, but an honest one. curl -fsSL https://install.kristoffer.dev/dotfiles without the pipe prints the actual script, fetched from this site. The list of packages it links is read from the same data file as the tables on this page, so the demo cannot drift from the documentation.
Troubleshooting
“Conflicts detected”. You ran the repo’s install.sh directly; it stops on a conflict. The one-liner moves conflicts aside first. Or move the file yourself and run it again.
Getting a backup back. Everything moved aside is in ~/.dotfiles-backup/<timestamp>/, laid out the way it was. mv it back.
oh-my-zsh.sh: no such file or directory. Oh My Zsh is not installed. Run the one-liner again with --tools.
The prompt shows boxes and question marks. The terminal font has no Nerd Font glyphs. Ghostty does out of the box; anything else needs a Nerd Font set by hand.
Starting over. Unlink everything, and the machine is back to where it was — minus the clone:
cd ~/dotfiles
stow -D -t ~/.config .config
stow -D -t ~/.claude .claude
sed -i.bak '/ZDOTDIR/d' ~/.zshenv