Feel free to use and adapt these configurations for your own setup.
# 1. Clone the repository
git clone https://github.com/froazin/dotfiles.git
cd dotfiles
# 2. Run the install script
chmod +x ./src/install.sh
./src/install.shThe installer configures whoever runs it. To keep sudo su and sudo -i in
zsh, make the system zsh (the one in /etc/shells, never one in your home)
root's login shell, then give root its own copy of the configuration:
sudo usermod -s /usr/bin/zsh root
sudo -H sh ./src/install.shsudo -s already starts your own shell, so it needs only the second line.
Root's files are copies rather than links to yours, so a process running as
you can't change what root runs. Run the install again after pulling changes.
If zsh is ever removed, set root back to bash first
(sudo usermod -s /bin/bash root), or su and sudo -i stop working. On WSL,
wsl -u root -e /usr/sbin/usermod -s /bin/bash root recovers it from Windows.
Each directory under src/modules/ is a module with a bootstrap.sh that
defines a bootstrap() function. A module is skipped entirely unless a command
matching its directory name is on PATH.
| Module | Installs |
|---|---|
claude |
~/.claude (mode 0700) with agents/, commands/, skills/, rules/, output-styles/ and hooks/, the commit-signing guard, the no-session-links hook, and a keybindings.json (Enter adds a new line, Ctrl+Enter or Shift+Enter sends) and settings.json merged into whatever is already there. Honours CLAUDE_CONFIG_DIR. |
git |
Global gitignore (core.excludesfile). |
podman |
A persistent registry credentials file, ~/.config/containers/auth.json (mode 0600), and ~/.docker/config.json as a link to it. |
zsh |
~/.zshenv, ~/.zshrc and the numbered ~/.zshrc.d/ drop-ins: prompt, completions, keybindings, aliases, the ws workspace picker and the first-login setup of a ways-of-working repository. |
A module is skipped outright unless the command matching its name exists, so that check is the only hard gate. Beyond it, dependencies fall into two tiers — neither prevents the configuration from being installed:
- Soft — the configuration works without them but loses real functionality. Reported as errors and the module reports failure, so the run exits non-zero and you notice. The install still completes, and the features start working on the next shell once the dependency is present — no reinstall needed.
- Optional — nice to have. Reported as warnings only, and not at all when
installing for root, which rarely has per-user tools like
uvornvm.
| Module | Requires | Soft | Optional |
|---|---|---|---|
claude |
claude |
jq, python3 |
— |
git |
git |
— | — |
podman |
podman |
— | — |
zsh |
zsh |
fzf, git, zsh-common, zsh-autosuggestions, zsh-syntax-highlighting |
op, uv, pulumi, direnv, nvm, ant, podman, xdg-open, gh |
The claude module never overwrites ~/.claude/settings.json or
~/.claude/keybindings.json, because Claude Code writes to the first
(/model, /config, /permissions, plugins) and you edit the second. Each
template is deep-merged in with jq: its values win, nested objects merge
key by key, lists are unioned, and every other key is kept. Keybinding
blocks merge by context, so the template's bindings go into your block for
the same context rather than a second one. Without jq a template is only
written where no file exists yet.
A ways-of-working repository holds your shared working practices for Claude
Code, with a user-config/ directory that belongs in ~/.claude. Put its
clone URL in ~/.config/ways-of-working/repo (the WSL user-data writes it
from the meta-data's ways_of_working):
mkdir -p ~/.config/ways-of-working
echo https://github.com/OWNER/ways-of-working.git > ~/.config/ways-of-working/repoUntil it's cloned, every zsh with a terminal runs wow-setup before its first
prompt:
- It clones the repository to
~/workspaces/<host>/<owner>/<repo>, wherewsfinds it. - If the clone fails and it's an
httpsGitHub repository, it runsgh auth login --webwhen gh isn't logged in yet, then clones it withgh repo clone. That's how a private repository gets cloned on a new machine. - It copies
user-config/into~/.claude(CLAUDE_CONFIG_DIRif set).
Once the clone exists, new shells skip it. Run wow-setup again after a
git pull to copy the changes, and delete the config file to stop a failed
setup from asking again. It never runs as root, or in a shell without a
terminal, such as the one VS Code starts to read your environment.
When commits are signed through something that needs you in person, such as
1Password with Windows Hello, an agent working while you're away hits a
signing timeout and tends to retry with signing switched off. The claude
module installs a guard against that: a hook,
~/.claude/hooks/commit_signing_guard.sh (logic in the .py beside it), and
a rule, ~/.claude/rules/commit-signing.md.
- It always denies signing bypasses:
--no-gpg-sign,tag --no-sign,-c/--config-env/GIT_CONFIG_*/HOME/sudooverrides of the signing settings, aliases that do any of that,git configchanges to them, edits of git config files, unsigned plumbing such ascommit-tree, and changes to the guard itself. There is no opt-out; to commit unsigned, run the command yourself. - When a git command fails to sign, or hangs until it times out, it tells the agent to stop and report, shows you a notice, and denies every signing git command in that session (subagents included) until your next message.
Commands are parsed like a shell (quotes, heredocs, $(...), sh -c), so
commit messages that mention these flags are fine. It needs python3 (the
standard library only) and does nothing without it. It is a guard rail
against an agent's habits, not a security boundary: a script on disk, another
interpreter or another git client can still get past it. Its tests are in
src/modules/claude/tests/.
Claude Code can add a link to its session (claude.ai/code/session_...) to
commit messages and pull requests, and in a public repository anyone can
follow it. The claude module turns that off with attribution.sessionUrl: false, and installs a rule, ~/.claude/rules/no-session-links.md, and a
hook, ~/.claude/hooks/no_session_links.sh (logic in the .py beside it),
that denies any tool call that would publish one another way.
- It looks for session URLs,
Claude-Session:trailers, session IDs andclaude.aiartifact links. - It checks Bash and Monitor commands, the message and body files they pass
to
gitandgh(-F,--file,--body-file,--notes-file), text written with Edit, Write, MultiEdit and NotebookEdit, and every MCP tool's input. - On
git push, it checks the messages of commits no remote has yet.
The Co-Authored-By: footer and the "Generated with Claude Code" line are
fine. Like the guard, it needs python3, does nothing without it, and is a
guard rail rather than a security boundary.
The module also sets attribution.pr to "", so Claude Code adds no
"Generated with Claude Code" line to pull request descriptions. Pull
requests credit Claude with the same Co-Authored-By: footer as commits.
ant is the Claude Platform CLI,
not the ant package from apt, which is Apache Ant. Its zsh completion is
generated on demand like the other optional tools.
With podman installed and its user API socket listening
(systemctl --user enable --now podman.socket), zsh points DOCKER_HOST at
that socket, so Docker clients such as docker compose or VS Code Dev
Containers use rootless podman. Root uses the system socket instead
(/run/podman/podman.sock, from sudo systemctl enable --now podman.socket).
A DOCKER_HOST you set yourself wins.
podman login saves credentials in $XDG_RUNTIME_DIR/containers/auth.json
by default, which lasts for the session. The podman module creates
~/.config/containers/auth.json, which persists, and links
~/.docker/config.json to it, so Docker tooling such as VS Code Dev
Containers (pulling private features) sees the same persistent credentials.
With podman installed and that file present, zsh exports
REGISTRY_AUTH_FILE pointing at it, so podman login, buildah and
skopeo read and write the persistent file instead. A REGISTRY_AUTH_FILE
you set yourself wins. An existing ~/.docker/config.json is never replaced,
and while it is a separate file zsh leaves REGISTRY_AUTH_FILE unset, since
setting it would stop podman reading that file's credentials: move its
auths (and any credHelpers) into podman's file, delete it, and rerun the
installer.
zsh-common is detected by probing zsh's own $fpath for vcs_info,
compinit and add-zsh-hook rather than by asking a package manager, so the
check works on any distribution.
The install scripts are POSIX
shso they run anywhere. Only the files they seed are zsh-specific.
src/wsl/Ubuntu-26.04.user-data is a cloud-init config that builds an Ubuntu
26.04 WSL instance from scratch:
- installs every package the modules above depend on, plus containers and a KVM/libvirt stack for running VMs inside WSL;
- enables podman's Docker-compatible API socket (
podman.socket): rootless for every user, and the system socket for root; - writes
/etc/wsl.confand disables swap to matchsrc/wsl/.wslconfig; - bridges Windows tools (VS Code, 1Password CLI, OpenSSH,
clip) into the LinuxPATH, and sets$BROWSERsoxdg-openopens things in Windows; - creates the user with zsh and passwordless sudo, when a username is given;
- installs
uv,pulumi,nvm, Claude Code (claude) and the Claude Platform CLI (ant) as that user, and the VS Code extensions listed in the meta-data'svscode_extensions(Claude Code and Python to start with) into the instance's VS Code server (this needs VS Code and its WSL extension on Windows); - configures git, then clones this repo and runs
install.sh. With agit_signingkey, commits are signed, and a literal key (not a key file path) goes in~/.config/git/allowed_signers, sogit log --show-signaturecan verify them; - with a
ways_of_workingclone URL, writes it to~/.config/ways-of-working/repo, so your first login sets up the ways-of-working repository; - makes zsh root's login shell and runs
install.shfor root too, sosudo sukeeps the same shell and prompt. Without a username, root's copy comes from a temporary clone.
The user-data holds nothing personal. Your username, git identity and VS Code extensions go in the meta-data, which cloud-init reads alongside it and which stays on your machine:
| Repo file | Copy to |
|---|---|
src/wsl/.wslconfig |
%USERPROFILE%\.wslconfig |
src/wsl/Ubuntu-26.04.user-data |
%USERPROFILE%\.cloud-init\Ubuntu-26.04.user-data |
src/wsl/Ubuntu-26.04.meta-data (fill it in) |
%USERPROFILE%\.cloud-init\Ubuntu-26.04.meta-data |
cloud-init matches both files by instance name, so install the distro under
its default name, Ubuntu-26.04, from PowerShell:
wsl --install Ubuntu-26.04cloud-init runs on first boot only, and the first launch waits for it. When it
finishes, restart the instance so the wsl.conf settings and the new group
memberships take effect:
wsl --terminate Ubuntu-26.04If the meta-data gives no username, WSL asks you to create a user as normal,
and that user gets zsh and the virtualisation groups. Root is already
configured. Finish setting up the user yourself:
curl -LsSf https://astral.sh/uv/install.sh | env UV_NO_MODIFY_PATH=1 sh
curl -fsSL https://get.pulumi.com | sh -s -- --no-edit-path
git clone https://github.com/nvm-sh/nvm.git ~/workspaces/github.com/nvm-sh/nvm
cd ~/workspaces/github.com/nvm-sh/nvm
git checkout "$(git describe --abbrev=0 --tags --match 'v[0-9]*' "$(git rev-list --tags --max-count=1)")"
# Installs that release into ~/.nvm. The zsh config from these dotfiles loads it, so leave the profiles alone.
PROFILE=/dev/null bash install.sh
cd
curl -fsSL https://claude.ai/install.sh | bash
v="$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/anthropics/anthropic-cli/releases/latest)"; v="${v##*/v}"
case "$(uname -m)" in aarch64) a=arm64 ;; *) a=amd64 ;; esac
mkdir -p ~/.local/bin && curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v$v/ant_${v}_linux_$a.tar.gz" | tar -xzmf - -C ~/.local/bin ant
# The extensions in your meta-data's vscode_extensions, e.g.:
code --install-extension Anthropic.claude-code --install-extension ms-python.python
git clone https://github.com/froazin/dotfiles.git ~/workspaces/github.com/froazin/dotfiles
# Until the ~/.zshenv from these dotfiles exists, the installer cannot see tools in ~/.local/bin.
PATH="$HOME/.local/bin:$HOME/.pulumi/bin:$PATH" sh ~/workspaces/github.com/froazin/dotfiles/src/install.sh
# With a ways-of-working repository, your next shell sets it up.
mkdir -p ~/.config/ways-of-working
echo https://github.com/OWNER/ways-of-working.git > ~/.config/ways-of-working/repoTo test changes, copy the files as ci-test.user-data and ci-test.meta-data
to match a throwaway instance:
wsl --install Ubuntu-26.04 --name ci-test
wsl --unregister ci-testHappy coding! 🚀