Scope: this file applies to any work an agent does inside this repo
(raxetul/dotfiles). Repo-local; nothing here is promoted to
~/.claude/. If you want to lift a rule out of this repo into a
global one, see doc/agentic-promotion.md.
-
Any new app config goes under
configurations/<app>/and is symlinked into the live tree byscripts/symlinks.sh. Add the mapping to theCOMMON_LINKS(or platform-specific) array in that script — never duplicate config content inline in setup code. Edits inconfigurations/take effect immediately, no re-run needed. -
Commit messages follow Conventional Commits. Types:
feat,fix,refactor,chore,docs,style,perf,build,ci,test,revert. Scope is optional but encouraged (feat(gpg): …,docs(roadmap): …). Thecommit-msglefthook rejects anything that doesn't match^(<type>)(\(<scope>\))?!?: <subject>. -
Line length is 120 columns. Never collapse a multi-line construct (shell heredoc, YAML loop, JSON dict) into one line just because it would fit — preserve the user's line breaks. This rule is repeated in the user's global
~/.claude/CLAUDE.md; it's loud here because most of this repo is heavily multi-line by design. -
Keep BOTH package docs in lockstep with the install lists. Adding a package to
packages/Brewfileor anypackages/*.list(apt.list,apt-desktop.list,pacman.list,pacman-desktop.list,dnf.list,dnf-desktop.list,aur.list,snap.list) requires a matching row in the same change in:doc/packages-native.md— the detailed reference: fill every manager column (brew / apt / pacman / dnf);—is fine when the package isn't in that repo, but then the Fallback column must say how it gets installed there.doc/packages-summary.md— the flat summary table (one row per package; per-OS cell = the install lane:brew/apt/arch/dnf/aur/snap/custom/… per its Legend). Removing or renaming a package updates both docs too.
The
post-tool-use.shhook flags aWrite/Editto a list that didn't touch either doc in the same turn. -
Preserve Nerd Font / PUA glyphs in
configurations/starship/starship.toml(and any other terminal config that uses them: tmux, ghostty, waybar, vim status lines). The prompt string,[os.symbols],[directory.substitutions], everysymbol = "…", the powerline separators(U+E0B0 / U+E0B4 / U+E0B6), the prompt chevrons❯❮(U+276F / U+276E), the clock glyph incmd_duration, and the time-icon glyph in[time].formatare load-bearing — stripping them produces a broken prompt with visible empty[]boxes.- Some agent input/output channels silently normalize Private Use
Area codepoints (U+E000–U+F8FF, plus most U+F000–U+FFFF Nerd
Font ranges) to empty strings. If you can't see a glyph in
the diff view of this conversation, assume it's there in the
file and don't "tidy" it. Hex-check with
xxdbefore concluding a line is empty.sed -n '78p' configurations/starship/starship.toml | xxd # if bytes between the quotes are `ee 9c 98` etc., the glyph is live
- When you need to edit such a file and the
Write/Editchannel is dropping glyphs, don't retype them by hand. Slice the glyph-bearing chunks out of the existing file (or out of git history viagit show <ref>:<path>) withsed -n 'A,Bp', and only construct the non-glyph structural lines viaprintfor here-docs. The byte-exact copy is the source of truth. - If you must add a brand-new glyph the file doesn't already
contain, fetch the literal UTF-8 byte sequence from the
upstream Nerd Font cheat sheet (
https://www.nerdfonts.com/cheat-sheet) or from another file in this repo that uses it. Never substitute a "close enough" ASCII fallback — the whole point of the Catppuccin Mocha terminal stack is that those glyphs render.
- Some agent input/output channels silently normalize Private Use
Area codepoints (U+E000–U+F8FF, plus most U+F000–U+FFFF Nerd
Font ranges) to empty strings. If you can't see a glyph in
the diff view of this conversation, assume it's there in the
file and don't "tidy" it. Hex-check with
-
Package install lane policy. Primary install vector is the native package manager:
brewon macOS,apt/pacman/dnfon Linux (selected by distro detection). Fall back to AUR (Arch-only —makepkg/yay/parubuilds, pacman-installable) or Snap (Debian/Fedora) only when the native repo lacks the package. Language-specific installers (cargo install,go install,pipx) and upstream release binaries are last-resort fallbacks, recorded in the Fallback column ofdoc/packages-native.md. Flatpak is intentionally out of scope — don't propose it. -
Package install lists live in
packages/at repo root, not underconfigurations/.configurations/<app>/is for app config files the user edits live (symlinked into~/.config/).packages/is the inventory of what gets installed (Brewfile+<pkgmgr>.listfiles +aur.list/snap.listfallbacks). The separation is load-bearing: a glance at the tree should show "configure" vs "install" without opening files. Don't put install lists back underconfigurations/and don't put.conf/.tomlconfig underpackages/. -
Footprint policy — user-scoped, easy uninstall. Every artifact the repo plants lives under
$HOME: symlinks under~/.config/and~/.vim*, scripts under~/.scripts/, plugin checkouts under~/.config/<tool>/plugins/or~/.local/share/. The only writes outside$HOMEare the native package manager doing its job (sudo apt install …,brew install …). Never plant files in/etc,/usr/local,/opt, or/varoutside what the package manager owns. Corollary: the repo shipsscripts/symlinks.sh uninstallthat strips every user-scope artifact in one pass; futurescripts/uninstall.sh(Phase 4 of v3-native) will wrap that with optionalapt purge/brew uninstalldriven by the.listfiles + optionalchshrevert. -
Custom-install hooks live in
packages/custom-install/<pkg>/, one folder per package, with REQUIREDbefore.shandafter.shslots.before.shruns before the package install step (register a third-party APT/COPR repo, accept an upstream key, pre-create a config dir);after.shruns after the install + AUR/Snap fallback (provision a toolchain — canonical example:rustup default stableto materializecargo/rustcin~/.cargo/bin/, enable a service, run a self-test).-
Both files MUST exist for every
<pkg>/directory, even when one side has no work. The unused side is a stub:#!/usr/bin/env bash+set -euo pipefail+exit 0. This keeps the layout uniform and the log timeline linear (every package contributes onebefore ----and oneafter ----banner per run, with(no-op stub)output when the script just exits). -
PATH additions go through
${DOTFILES_DIR}/.path. When the package adds binaries to PATH (rustup →~/.cargo/bin, future tools likewise) or defines aFOO_HOME-style env var, theafter.shMUST write a segment into the gitignored${DOTFILES_DIR}/.pathfile, bracketed by markers so re-runs are idempotent:# >>> rustup begin [ -d "${HOME}/.cargo/bin" ] && case ":${PATH}:" in *":${HOME}/.cargo/bin:"*) ;; *) PATH="${HOME}/.cargo/bin:${PATH}"; export PATH ;; esac # >>> rustup end
The hook strips any existing
# >>> <pkg> begin … endsection before appending the fresh one —sed -i.bak '/^# >>> <pkg> begin$/,/^# >>> <pkg> end$/d'..pathis sourced from.load, which is sourced fromconfigurations/{zsh,bash}/rc. Both.loadand.pathare gitignored (per-host);scripts/init-loadensures.loadexists. Don't put one-offPATH=…lines in the shell rc files; they belong in.path. -
Every script must be executable, idempotent (re-running with the work already done is a no-op), skip cleanly if the package isn't installed, honor
DRY_RUN=1, and use$DOTFILES_DIRto find anything in the repo.
setup.sh(Step 2 before-pass, Step 5 after-pass) andscripts/update-dotfiles(stage_packages_custom_before/stage_packages_custom_after, addressable via--only=custom-install,--only=custom-install-before, or--only=custom-install-after) iterate the directory in lexical order throughscripts/run-custom-install-hook, which logs each hook to~/.local/state/dotfiles/custom-install.logand streams to the terminal. Seepackages/custom-install/README.mdfor the full contract. Anything that's configuration (lives in~/.config/<app>/) belongs inconfigurations/<app>/, not here. -
-
.pathis the single source of truth for per-package PATH and env. Centralized, clean, one bracketed segment per package. Any package that installs binaries outside the bootstrap dirs, or that needs aFOO_HOME-style env var, gets exactly one segment in the gitignored${DOTFILES_DIR}/.path:# >>> <pkg> begin export FOO_HOME="${HOME}/.foo" [ -d "${FOO_HOME}/bin" ] && case ":${PATH}:" in *":${FOO_HOME}/bin:"*) ;; *) PATH="${FOO_HOME}/bin:${PATH}"; export PATH ;; esac # >>> <pkg> end
- The segment is written by that package's
custom-install/<pkg>/after.sh(rule #9), which strips the old segment (sed -i.bak '/^# >>> <pkg> begin$/,/^# >>> <pkg> end$/d') and re-appends before adding the fresh one — idempotent..pathis sourced by.load, which is sourced byconfigurations/{zsh,bash}/rc. - The
[ -d ]andcaseguards are mandatory — they keep the segment a no-op on hosts where the dir doesn't exist and idempotent across shell reloads. A segment may be written on every host even if only one platform needs it (the guards make the others harmless), so behavior doesn't fork per distro. - The only non-package PATH entries are the generic bootstrap
dirs
~/.scriptsand~/.local/bin, set once in.load(viascripts/init-load). A tool that installs into~/.local/binneeds no segment; one that installs elsewhere (atuin →~/.atuin/bin, rustup →~/.cargo/bin+ toolchain) does. - Never put
PATH=…/export FOO_HOME=…lines insetup.sh,scripts/*,configurations/{zsh,bash}/rc, or any other shell file. If a tool needs a path, it gets a.pathsegment via itsafter.sh. No exceptions — that's what "centralized" buys us.
- The segment is written by that package's
-
Reference the home directory through
${HOME}, never a static path. Anywhere a path under the user's home is needed — in anyconfigurations/<app>/file, in.load, in.path, and in the scripts/hooks that generate them — write${HOME}/…(or$HOME/…), never a hardcoded/home/<user>/…or/Users/<user>/…. The same config is symlinked across hosts and both OSes where the home root differs (/home/emrahvs/Users/emrah), so a baked-in absolute path breaks portability and the one-pass uninstall.- Prefer the braced
${HOME}form for quoting safety; bare~only expands unquoted and at word start, so it silently fails inside quotes or mid-string — don't rely on it in config files. - For the repo root specifically, use the
${DOTFILES_DIR}variable (which itself defaults to${HOME}/gel-ort/dotfiles), not a literal path — see.load/scripts/init-load.
- Prefer the braced
-
Migrating a live config into the repo follows one fixed procedure — the same one used for
claudeandghostty. To bring an app's existing config under management:- Probe for secrets and runtime state first. Move only the
files the user hand-edits; never the credential/cache/history
artifacts (the
claudemigration deliberately left.credentials.json,history.jsonl,projects/,sessions/behind). If a file's sensitivity is unclear, ask before moving. - Move (don't copy) the file into
configurations/<app>/, preserving its path layout under~/.config(a nested~/.config/<app>/sub/foo→configurations/<app>/sub/foo). - Symlink it back with
ln -sfn <repo-src> <live-dst>so the user's edits keep taking effect live. - Register the mapping in the
COMMON_LINKS(or a platform-specific) array inscripts/symlinks.sh, with a${HOME}-relativedst(rule #1, #11). A whole directory may be linked as one entry (seescripts::.scripts). - Verify:
scripts/symlinks.sh listshows the new entry andreadlinkon the live path resolves into the repo. The/migrate-configcommand automates steps 2–5 (and prompts on step 1). Use it rather than doing the moves ad hoc.
- Probe for secrets and runtime state first. Move only the
files the user hand-edits; never the credential/cache/history
artifacts (the
-
Centralized Claude commands and rules are always tracked. Every slash command under
configurations/claude/commands/and every global rule inconfigurations/claude/CLAUDE.mdmust be committed — never left untracked. Both are symlinked globally (the wholecommands/dir andCLAUDE.mdinto~/.claude/), so an untracked file works on this host but silently vanishes on a fresh clone. When a new command or global rule appears there (often authored live via~/.claude/), commit it in the same pass; don't leave it dangling. -
Plugin sets are declarative — the repo config is the source of truth, so reconcile (install declared + prune undeclared) on every update. For any app whose plugins are managed by a manifest in
configurations/<app>/(tmux's@pluginlines intmux.conf, vim'sPlugdirectives invimrc, and any future plugin-managed tool), the update path must do both halves: install the plugins the config declares and delete the on-disk checkouts it no longer declares. Installing-only lets a dropped plugin (e.g. tmux-resurrect / tmux-continuum) keep running from its stale~/.config/<app>/plugins/…or~/.vim/plugged/…dir long after the@plugin/Plugline is gone — the classic "I removed it from the config but it's still active" bug. The reconcile lives inscripts/update-dotfiles'srun_configurationsstages, using each manager's own prune verb, never a hand-rolledrm:- TPM →
bin/install_pluginsthenbin/clean_plugins(stage_configurations_tpm). - vim-plug →
vim +PlugInstall +PlugClean! +qa --headless; the!skips the interactive confirm so it's headless-safe (stage_configurations_vim_plug). Adding a new plugin-managed app means wiring the same two-step (install + manager-native clean) into its ownrun_configurationsstage.setup.shonly bootstraps the managers; the install/prune reconcile is an update concern, reachable via/update(orupdate-dotfiles --only=configurations).
- TPM →
-
Project initialization is the
/init-proj-*command family, and per-project standards live in the project — never in the global Claude config. The aim is a small global footprint: a rule loads only inside the project it governs, so each session's context stays lean. The family and its layering are documented indoc/init-proj.md— keep that doc in lockstep with the command files (same spirit as rule #4 for packages)./init-proj-commonwrites the shared baseline into a project's own./CLAUDE.md+ scaffolds it:git init, lefthook with conventional-commitcommit-msg+pre-commit, and the common rules (dependency injection, unit testing, logging, pre-CLI briefs).- Each
/init-proj-<type>(backend,frontend,embedded-firmware,kernel-driver,cli,desktop,mobile) runscommonfirst (idempotent — it skips steps already done), then layers type-specific rules and scaffolding, invoking the standalone building blocks (/logging,/rfc9457,/backend-stack) rather than duplicating their rule text. common's features are named and overridable (defaults all on); a type command may disable the ones that don't fit and supply its own replacement — e.g.kernel-driverdisableslogging,dependency-injection, andunit-testingin favor ofpr_*,ops-struct seams, and KUnit./init-proj-monorepoasks which types to include, lays down one root baseline, and gives each package its own nestedCLAUDE.mdvia the matching type command.- Every side-effecting step is previewed as a pre-CLI brief table and every destructive one is confirmed before running. New types follow this layering; new command files are committed (rule #13).
- OS detection in shell scripts: branch on
$(uname)(DarwinvsLinux), then for Linux distros source/etc/os-releaseand case on${ID}.scripts/symlinks.shandsetup.share the reference implementations. - Backup scope: only configs this repo manages. Anything else
under
~/.config/is left alone.scripts/backup-configs.shis the authority on what's in scope. - Theming: terminal apps use Catppuccin, dark Mocha flavour by
default. The one intentional exception is tmux, which runs the
light Latte flavour (
@catppuccin_flavour 'latte'+configurations/themes/tmux/catppuccin-latte.conf) for a light-background status bar. When adding a new app, source its palette fromconfigurations/themes/if a palette file is needed, not from inline hexes. - Aliases: cross-shell via portable
alias name='cmd'syntax inconfigurations/aliases/*.sh, sourced from both zsh and bash. No third-party multi-shell alias manager.
See .claude/commands/ for the full set. Quick map:
| Command | What it does |
|---|---|
/apply |
Runs setup.sh. Forwards --desktop / --update when asked. |
/update |
Runs scripts/update-dotfiles. Forwards --dry-run / --desktop / --only=… when asked. |
/commit |
Builds a Conventional Commit message from git diff --cached. |
/check |
Runs shellcheck scripts/*.sh setup.sh, validates packages/*.list, and commitlint --from origin/main. |
/migrate-config |
Brings a live ~/.config/<app> config under the repo: probe → move → symlink back → wire COMMON_LINKS → verify (per hard rule #12). |
doc-author— keepsdoc/*.mdfiles in lockstep with the scripts and configurations they document. Read when you changesetup.sh,scripts/*.sh, or apackages/*.listand need to refresh its companion doc.
pre-commit.sh— wrapslefthook run pre-commit. The lefthook pre-commit pipeline already runsshellcheck, so this is a thin shim invoked from/commit.commit-msg.sh— validates a candidate commit message against the same regexlefthook.ymluses, before the agent actually runsgit commit. Lets/commitreject its own draft and retry.post-tool-use.sh— registered as aPostToolUseClaude Code hook in.claude/settings.json, filtered toWrite/Edit. If the touched file is apackages/*.listorpackages/Brewfileand the same edit didn't also touchdoc/packages-native.md, it prints a warning (non-blocking) so the doc doesn't silently rot.