This file provides guidance to coding agents (Claude Code, Codex, Copilot CLI, OpenCode, …) when working with code in this repository.
Read README.md first — it covers the user-facing flow: client roles, the role-layer table (bootstrap / base / server_hardening / terminal_dotfiles / personal / graphical_dotfiles / os_macos / coding_agents), and bootstrap/usage commands. This file only documents the non-obvious architecture and editing constraints that aren't in README.
Personal configuration monorepo driving three loosely-coupled subsystems:
- Ansible (
ansible/) — the active provisioning layer, shared across all hosts (Linux and macOS). - NixOS (
nixos/) — declarative system config for the NixOS machines (artus,margo,jolly, …). Not every host uses it. - Dotfiles (
dotconfig/) — raw config trees (fish/nvim/kitty/hypr/lf/broot) consumed by the Ansible roles via symlink.
bootstrap.sh is the green-field entrypoint; coding-agents/ is a generator that fans out one agent definition to four CLIs.
-
Single entrypoint.
ansible/site.ymlloops over each host'shost_roleslist and applies roles individually, tagging each by role name. There is no per-host playbook — host identity comes fromansible/inventory/host_vars/<name>.yml, which setshost_rolesplus host-specific vars. -
Push vs. pull duality. Push (
ansible-playbook -l <host>) uses the inventory. Pull (ansible-pullon-host) setsinventory_hostname=localhost, so host_vars are resolved via-e host_id=<name>and an explicitinclude_varsinsite.yml's pre_tasks. When touchingsite.ymlor host_var loading, keep both paths working. -
Role layering intent.
baseis universally safe — never add firewall, hostname rewrite, swap, or other destructive defaults to it. Those belong inserver_hardening. Theterminalhost profile (host_vars/terminal.yml) deliberately excludespersonalandserver_hardeningso it's safe to apply on any unfamiliar VM. -
confirm_overwrite=falseby default.terminal_dotfilesskips symlinking over real config directories (fish/kitty/nvim/broot create their own on first launch). Pass-e confirm_overwrite=trueto force; ignored under--checkfor safety. -
group_vars/all.ymlholds shared identity + cross-role config (git identity,home_dircomputed from OS,repos_list,repo_root). Roles read these rather than redefining defaults. -
Roles describe a clean install, not migrations. Never add tasks whose only purpose is removing/renaming legacy state that a fresh install wouldn't have (stale symlinks, renamed configs). One-time cleanups are done manually per host.
Validating changes: there are no tests/lint. Use --check --diff against a real host, or ./scripts/ansible_smoketest.sh. Subset by role with --tags <role>.
README covers the high-level rules (live entrypoint is /etc/nixos/configuration.nix, host-specific behavior under nixos/machines/, shared desktop plumbing under nixos/shared/, jolly upgrades via next-boot not live-switch). Beyond that:
nixos/shared/is split intobase-system.nix,networking.nix,desktops/(Hyprland),programs/,packages/,policy/,users/,hardware/. New cross-host plumbing belongs here.- Repo-root
configuration.nixis a reference entrypoint, not authoritative for every machine. Machine-localhardware-configuration.nixstays on the host. - See
docs/jolly.mdfor the recovery context behind the boot-vs-switch rule.
dotconfig/hypr/hyprland.lua replaced hyprland.conf. This also changes every
external caller, which is easy to miss: with a Lua config root, hyprctl's
parser is Lua too.
hyprctl dispatch <name> <args>→hyprctl dispatch '<lua expr>', e.g.hyprctl dispatch 'hl.dsp.window.float({ action = "toggle" })'. This applies to every dispatcher, including argument-less-looking ones likedpms on.hyprctl keyword a:b v→hyprctl eval 'hl.config({ a = { b = v } })'(keyworderrors with "can't work with non-legacy parsers").- exec with window rules:
hyprctl eval 'hl.exec_cmd([[cmd]], {workspace = [[3 silent]]})'— the bracket contents become the rule table value, and{silent = true}is rejected ("unknown effect"). hyprctlexits 7 on a parse error. Check it in scripts: the failure mode is otherwise completely silent (this is how the whole login autostart broke).
hl.dsp.dpms needs a table: hl.dsp.dpms({ action = "on" }). A bare string
leaves action nil and turns the display off. On jolly that is not
recoverable — the monitor is also the USB KVM hub, so ~90s without a DP signal
makes it auto-switch input and take the keyboard, audio and ethernet with it.
Never probe dpms against the real display; hyprctl output create headless gives
you a throwaway monitor to test against.
A generator: one source file per agent fans out to four CLIs (Claude Code, Codex, Copilot CLI, OpenCode). The full guide is coding-agents/README.md — read it before creating or editing any agent or skill. The essentials:
- Edit only
coding-agents/source/<name>.md. It's opencode-flavored: YAML frontmatter + a markdown body that is the agent's prompt.build.pyrenders per-tool variants intocoding-agents/generated/(gitignored); thecoding_agentsAnsible role runsbuild.pythen symlinks them into~/.claude/agents/,~/.config/opencode/agents/,~/.codex/agents/,~/.copilot/agents/. Never hand-editgenerated/— it's rebuilt every run. mode:decides what you get.mode: subagent(default) → invoked by another agent via the Task/Agent tool, runs in its own context, can't pause for the user — use for delegated, self-contained jobs (a reviewer, a specialist).mode: primary→ ALSO rendered as a Skill (/<name>) for Claude Code + Codex, runs in the main thread, can pause for sign-off / clarifying questions while still delegating to subagents — use for an interactive workflow (e.g.ic).- Permissions translate to real restrictions per tool.
permission: { edit|bash|webfetch: allow|ask|deny }. Adenyis enforced — e.g.edit: denystrips Edit/Write/NotebookEdit from the Claude variant,webfetch: denystrips WebFetch/WebSearch. Full translation table incoding-agents/README.md. descriptionshould start with "Use when …" — it's what each tool reads to decide when to pick the agent.- Deleting
source/<name>.mddoes not auto-remove the existing symlinks — clean those up by hand. - After editing, rebuild + resync:
ansible-playbook ansible/site.yml -l <host> --tags coding_agents(or justpython3 coding-agents/build.pyto verify rendering without installing).
Commit messages: scope prefix first, then short imperative summary — e.g. ansible:, nixos:, fish:, claude:, kvm:. Match the existing style in git log.
For simple, self-contained config changes (e.g. adding a package to a host, flipping a flag, bumping a value), commit the change yourself without being asked — stage only the files you touched, leave unrelated working-tree changes alone, and use the convention above. Surface anything non-trivial (architectural choices, destructive ops) before committing.