Architecture
This document explains how the dotfiles repository is organized, how configuration reaches your home directory, how shells start up, and how the project stays secure and consistent through CI/CD.
Table of Contents
- Deployment Models
- XDG Base Directory Hierarchy
- Shell Startup Flow
- AI Tool Permission Model
- CI/CD Pipeline
- Adding New Tools
Deployment Models
The repository supports two deployment strategies, each with different trade-offs.
Dotter (Primary)
dotter is the recommended deployer. It supports profiles, conditional Windows paths, and templating.
# .dotter/global.toml (excerpt)
[default]
depends = ["agent", "editor", "shell", "terminal"]
[agent]
depends = ["claude", "codex", "kimi", "opencode"]
[editor]
depends = ["helix", "zed"]
[shell]
depends = ["bash", "powershell", "zsh"]
[terminal]
depends = ["alacritty", "windows-terminal"]Typical workflow:
just deploy check # dry-run preview (mise run deploy:check)
just deploy apply # dotter deploy --force (mise run deploy:apply)
just deploy undeploy # dotter undeploy (mise run deploy:undeploy)Activate an optional profile at any time:
dotter deploy -p bashWindows-only paths use if = "dotter.windows":
[alacritty.files]
"dotfiles/.config/alacritty/alacritty.toml" = {
target = "~\\AppData\\Roaming\\alacritty\\alacritty.toml",
type = "symbolic",
if = "dotter.windows"
}NixOS / Home Manager
For NixOS, nixos/home.nix creates a fixed list of out-of-store symlinks using config.lib.file.mkOutOfStoreSymlink. Every configured path is asserted to exist at evaluation time:
home.file = builtins.listToAttrs (
map (target: {
name = target;
value = { source = link "${dotfilesRoot}/${target}"; };
}) dotfileLinks
);This path is useful when the Nix flake is the source of truth for the machine, but it covers only the Linux/Wayland subset of the dotfiles.
Windows vs Linux Differences
| Concern | Linux / macOS | Windows |
|---|---|---|
| Deployer | dotter | dotter |
| Config path | ~/.config | ~/AppData/Roaming for many GUI apps |
| Shell | Nushell, Bash, Zsh | Nushell, PowerShell, Bash (Git Bash) |
| WM | Hyprland | Windows window manager |
| Benchmarks | just bench skips shells that are not installed | same |
XDG Base Directory Hierarchy
The configuration enforces XDG Base Directory compliance to keep $HOME clean.
# dotfiles/.config/nushell/env.nu (excerpt)
$env.XDG_CONFIG_HOME = ($env.XDG_CONFIG_HOME? | default ($nu.home-dir | path join ".config"))
$env.XDG_CACHE_HOME = ($env.XDG_CACHE_HOME? | default ($nu.home-dir | path join ".cache"))
$env.XDG_DATA_HOME = ($env.XDG_DATA_HOME? | default ($nu.home-dir | path join ".local" "share"))
$env.XDG_STATE_HOME = ($env.XDG_STATE_HOME? | default ($nu.home-dir | path join ".local" "state"))| Purpose | Default path | Example contents |
|---|---|---|
| Config | ~/.config | Editor, shell, terminal, AI tool configs |
| Data | ~/.local/share | Tool data, plugin state, vendored assets |
| Cache | ~/.cache | Generated completions, download caches |
| State | ~/.local/state | History, persistent sessions |
| User binaries | ~/.local/bin | Personal scripts and manually installed tools |
Sensitive files such as SSH keys and OAuth tokens are stored outside the repository and referenced by path, never embedded in config files.
Shell Startup Flow
Each shell has its own startup chain. Keeping the chains short and modular makes startup fast and debugging easy.
Bash
~/.bash_profile
→ dotfiles/.config/bash/bash_profile
→ loads profile.d/*.sh (XDG, PATH, environment)
→ ~/.bashrc
→ dotfiles/.config/bash/bashrc
→ loads shell/shell.d/*.sh
→ loads bash/bash.d/*.bashZsh
~/.zshenv
→ sets ZDOTDIR and XDG variables
~/.config/zsh/.zprofile
→ loads shell/profile.d/*.sh
~/.config/zsh/.zshrc
→ loads shell/shell.d/*.sh
→ loads zsh/zsh.d/*.zshNushell
~/.config/nushell/env.nu
→ XDG, PATH, tool-specific environment
~/.config/nushell/config.nu
→ core settings
→ use modules/*.nu
→ generate autoload configs for Starship, Zoxide, Carapace on first runPowerShell
~/.config/powershell/profile.ps1
→ ~/.config/powershell/config.ps1
→ XDG setup
→ cached tool inits
→ aliases and functionsShared POSIX logic lives in dotfiles/.config/shell/, while shell-specific logic lives in dotfiles/.config/bash/, dotfiles/.config/zsh/, and dotfiles/.config/nushell/.
AI Tool Permission Model
Four AI agents are configured: Claude Code, Codex, Kimi Code, and OpenCode. All four use a default-deny permission model.
Permission Categories
| Category | Typical Default | Description |
|---|---|---|
| Read | Allow / Deny | Read source files; deny secrets and private keys |
| Write | Ask | Create or modify files |
| Execute | Ask | Run shell commands, build scripts, tests |
| Network | Ask | Fetch URLs, install packages, call APIs |
| Destructive | Deny | rm -rf, git push, kubectl delete, publishing |
Examples from Configs
OpenCode denies dangerous bash patterns and asks for edits:
// dotfiles/.config/opencode/opencode.jsonc
"permission": {
"*": "ask",
"bash": {
"*": "ask",
"rm -rf *": "deny",
"git push*": "deny",
"git status*": "allow"
}
}Claude Code lists allowed read-only commands and denies secret paths:
// dotfiles/.config/claude/settings.json (excerpt)
"permissions": {
"allow": ["Bash(git status:*)", "Bash(git diff:*)", "Bash(just fmt:*)", ...],
"deny": ["Read(**/.env)", "Read(**/*.pem)", "Bash(rm -rf:*)", "Bash(git push*:*)"]
}Kimi Code uses ordered rules so sensitive-file denies are evaluated before the broad Read allow:
# dotfiles/.config/kimi-code/config.toml (excerpt)
[[permission.rules]]
decision = "deny"
pattern = "Read(*.env)"
reason = "Block reading local env files."
[[permission.rules]]
decision = "allow"
pattern = "Read"
reason = "Safe read-only file access."Codex uses workspace sandboxing with explicit filesystem globs:
# dotfiles/.config/codex/config.toml (excerpt)
[permissions.workspace.filesystem]
":workspace_roots" = { "." = "write", "**/.env" = "deny", "**/*.key" = "deny" }Why Ask vs Deny?
- Ask is used for state-mutating operations (writes, shell execution, network). This keeps the agent helpful while preventing silent changes.
- Deny is reserved for irreversible or high-risk actions (deleting files, force pushes, privilege escalation) and for reading sensitive material (keys, credentials, history).
- Allow is limited to read-only inspection commands that are safe to run repeatedly, such as
git status,git diff, andjust fmt.
CI/CD Pipeline
GitHub Actions (.github/workflows/ci.yml) runs in three stages.
Stage 1: Quality Gates
All jobs run in parallel and must pass before Stage 2.
| Job | Purpose |
|---|---|
guard | Classify the run as trusted or fork; every other job depends on it |
fmt | Check dprint, stylua, shfmt, and Justfile formatting |
lint | Run yamllint, actionlint, shellcheck, selene, markdownlint, etc. |
security | Secrets, SAST, HTML/SVG rules, Trivy, OSV, Grype, Grant, OPA policies, zizmor |
codeql | Semantic analysis of GitHub Actions workflows and TypeScript sources |
dependency-review | Block dependency changes with known high-severity advisories (pull requests) |
antivirus | ClamAV malware scan |
docs | aube install/audit, VitePress build, Lychee link check, site hardening checks |
guard publishes a trusted output that gates every privileged step. A fork runs all of the above and none of the steps that write to the repository — no SARIF upload, no pull-request comment, no deploy.
Vulnerabilities, licenses and exploitability
Three questions, three tools, one document that says when an answer does not apply here:
| Question | Tool | Configuration |
|---|---|---|
| What is in it? | Syft (from the artifact pipeline) | — |
| Is it vulnerable? | Trivy, OSV and Grype | .grype.yaml |
| Which licenses are in it? | Grant, plus Trivy's license scanner | .grant.yaml |
| Does it apply to us? | OpenVEX | misc/vex/dotfiles.openvex.json |
Findings are never silenced with an ignore file. A suppression is a VEX statement with an author, a timestamp, the affected package and an OpenVEX justification, and policy/vex.rego fails the build if one of those is missing.
The license question is answered for the whole deployment, not just the committed tree. vendir pulls in tmux plugins, yazi flavors and wallpapers that are gitignored but end up in $HOME, so the security job syncs them and mise run security:licenses reports every component with its license and origin, warning on anything outside MIT and Apache-2.0. The check refuses to run against an unsynced tree instead of quietly reporting on half of it.
The documentation site is covered too: docs/src/public/_headers ships a Content-Security-Policy and the usual hardening headers, misc/semgrep/web.yaml rejects executable markup in SVG and Markdown, and mise run docs:security proves the headers reached docs/dist and that no bundled JavaScript carries a known advisory.
Stage 2: Validation and Performance
Both jobs run only after Stage 1 succeeds and use tools pinned by mise — no Nix, no stow.
| Job | Purpose |
|---|---|
validate | mise run deploy:apply (dotter), install the global toolchain, then mise run validate:all |
performance | mise run bench:setup then mise run bench:ci; compares shell startup ratios with the baseline |
validate:all covers AI agents, MCP servers, editors, shells, multiplexers, and CLI tools; for example hx --health all for Helix, zellij setup --check for Zellij, and yazi --debug for Yazi.
Stage 3: Deploy
Pull requests get a Cloudflare Pages preview (mise run deploy:cloudflare:preview). On pushes to main (not scheduled), the docs go to Cloudflare Pages production and to GitHub Pages.
Adding New Tools
When adding a new tool config, follow this checklist to keep deployment, validation, and documentation in sync:
- Create the config file under
dotfiles/.config/<tool>/. - Map it for deployment in
.dotter/global.toml. - Add a NixOS link in
nixos/home.nixif the tool is Linux/Wayland relevant. - Add a validation task (
validate:<group>:<tool>) inmise.tomland list it invalidate:all. - Document hotkeys in
docs/src/<section>/<tool>.mdand add the entry todocs/src/cheatsheet.md. - Update the docs navigation in
docs/.vitepress/config.mts. - Run
just fmtandjust lintbefore committing.
This keeps the repository self-describing: every deployed file has a documented path, a validation step, and a place in the architecture.