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:

  1. 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.
  2. Clones the repo into ~/dotfiles. If it is already there, it fast-forwards it instead, and leaves it alone if it has uncommitted changes.
  3. 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.
  4. 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.
  5. 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
FlagDoes
--dry-runPrints every change instead of making it — including what would be moved aside
--toolsAlso installs zsh, tmux, Neovim, lazygit, Ghostty (macOS), Oh My Zsh and Powerlevel10k
--no-depsInstalls nothing; fails if git or Stow is missing
--dir DIRClones somewhere other than ~/dotfiles
--branch BUses 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

  1. xcode-select --install — gets you git.
  2. Install Homebrew. The installer finds it and prefers it from then on.
  3. Run the one-liner with --tools. On a Mac that includes Ghostty, as a cask.
  4. Open Ghostty instead of Terminal.app. It ships with a Nerd Font built in, which is what makes the Powerlevel10k prompt render properly.
  5. exec zsh. The first start runs the Powerlevel10k wizard — answer it once.
  6. nvim. The first launch installs every plugin. Give it a minute and do not panic at the wall of text.
  7. Add an SSH key, then gh auth login.

Ubuntu, Debian and WSL

  1. sudo apt install -y curl
  2. 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.
  3. Run the one-liner with --tools.
  4. chsh -s "$(command -v zsh)", then log out and in again. The installer reminds you if the login shell is not zsh yet.
  5. 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:

PackageLinked toWhat it brings
zsh~/.config/zshOh My Zsh + Powerlevel10k, aliases, tmux helpers, CLI completions
tmux~/.config/tmuxCtrl+a prefix, vim-style panes, mouse on, OSC 52 clipboard
nvim~/.config/nvimLazyVim with .NET, Go, Vue, Tailwind, DAP and Claude Code extras
ghostty~/.config/ghosttyCatppuccin Mocha, clipboard read/write allowed
lazygit~/.config/lazygitNerd Font v3 icons
opencode~/.config/opencodeNine agents (companion, reviewer, architect …) and skills
claude~/.claude/settings.jsonClaude 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:

AliasRunsWhy
nnvim .Open the editor on the current directory
llls -alLong listing, dotfiles included
gssgit status -sShort status, the one I type most
gaagit add .Stage everything
gcmgit commit -mCommit with a message
gpshgit pushPush
cclaude --allow-dangerously-skip-permissionsClaude Code, no prompts — only in a sandbox
ccclaudeClaude Code, with prompts
ocopencodeOpenCode
dtabledocker ps --format table …Containers as a readable table
dstartdocker compose up -d && docker compose logs -fStart the stack and tail it
tmxtmx <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:

KeysDoes
Ctrl+aPrefix (instead of Ctrl+b)
Ctrl+a |Split left/right
Ctrl+a -Split top/bottom
Ctrl+a h/j/k/lMove between panes
Ctrl+a [Copy mode, vi keys
Ctrl+a rReload 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:

  • alias lists them all
  • ll ~/.config shows the links Stow made
  • cat ~/.config/zsh/.zshrc prints what is in them
  • n opens Neovim, as far as the splash screen
  • tmux and neofetch do what you would expect
  • dotfiles diff lines 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