Load BEFORE proposing any structural or design change to the dotfiles repo, and whenever you catch yourself asking "why is it built this way", "can I just track this file", "why is settings.json not symlinked", "why two config dirs", "why is this untracked", or "can the SessionStart hook install the plugin". Documents the eight load-bearing design decisions with rationale, the invariants that must hold, and the known weak points, plus the install-layout reference (install flow, symlink and seed targets, hook catalogue) and the Codex plugin integration reference (codex-surfaces.py / codex-roles.py reconciliation, staging provenance). NOT for making/committing a change (dotfiles-change-control), diagnosing a breakage (dotfiles-debugging-playbook), or the blow-by-blow incident history (dotfiles-failure-archaeology).
Recursos
1Instal·la
npx skillscat add taloncjones/dotfiles/dotfiles-architecture-contract Instal·la des del registre de SkillsCat.
Dotfiles Architecture Contract
The load-bearing design decisions of this repo, each with the WHY that makes it
non-obvious, plus the invariants any change must preserve and the weak points
we know about and have chosen to live with (for now). Every decision here was
paid for -- most have a commit-hash-cited incident behind them (details in
dotfiles-failure-archaeology). If a proposed change violates a decision below,
the change is wrong until proven otherwise.
Jargon, defined once:
- Symlinked asset: a file/dir in this repo that
install/common/link.sh
(orbootstrap-cloud.sh) links into$HOME-- edits to the repo take effect
live on the machine. - Seeded machine-local file: a real file copied from a repo template once,
then owned by the machine; the repo never sees its contents again. - Vendored-untracked: upstream content copied onto disk inside the repo
tree by an installer, deliberately gitignored. - Superpowers (retired 2026-09):
superpowers@claude-plugins-official(retired), a Claude Code plugin this repo used to depend on, like the retired ECC (formerlyecc@eccfrom github.com/affaan-m/ECC).
Front-loaded warnings
- [WARNING] This repo is PUBLIC. Decision 8 below is not one constraint among
eight -- it shapes the other seven. Anything employer-specific, secret, or
machine-specific must be structurally incapable of landing here. - [WARNING] Never convert a seeded machine-local file into a symlink "for
convenience". That exact move corrupted state before: Claude CLI wrote
through a stale symlink and polluted the repo / dropped plugin config
(guards and history ininstall/common/claude-links.sh,seed_machine_local_file). - [WARNING] Never rely on a SessionStart hook to make a plugin usable in the
same cloud session. Plugins load at CLI launch; post-launch installs are
dark until the NEXT session (commit c1c4500). See Decision 5.
Decision 1: symlink vs seed -- two classes, never blur them
| Class | Examples | Why |
|---|---|---|
| Symlinked (repo-owned, live) | zsh/ -> ~/.zshrc; claude/{global-instructions.md,commands,agents,hooks,skills,rules} -> ~/.claude*/; git/hooks -> ~/.config/git/hooks; bin/* -> ~/bin/ |
One source of truth; git pull updates every machine; edits are testable in-repo |
| Seeded machine-local (template copied once) | claude/settings.json.tmpl -> ~/.claude*/settings.json; git/work/.gitconfig-work.tmpl -> ~/.gitconfig-work; ssh/configs/agent.toml -> 1Password agent config; ~/.ssh/config_local |
The live file is WRITTEN BY SOMETHING ELSE (plugin installers write settings.json) or holds data that must never be public (employer identity, vault names, lab hosts) |
The rule: if any tool writes into the file at runtime, or if its real contents
are private, it is seeded -- never symlinked. A symlink there means writes go
through into the public repo. seed_machine_local_file ininstall/common/claude-links.sh enforces this: it removes any pre-existing
symlink at the destination, seeds only if absent, and warns when the existing
file is under 50% of the template size (the signature of a prior write-through
corruption).
Cost accepted knowingly: template changes do not propagate to existing
machines (see Weak points).
Decision 2: one asset source, two config dirs
~/.claude (personal account) and ~/.claude-work (work account) are both
populated by link_claude_config_dir in install/common/claude-links.sh from
the SAME claude/ tree. Each dir keeps only its own machine-localsettings.json and its own plugin installs.
Why: accounts must be isolated (separate OAuth, separate plugin state) but the
assets -- CLAUDE.md, skills, hooks, rules -- must never fork. Duplicate asset
trees would drift silently.
Routing: the claude() wrapper in zsh/claude-account.zsh setsCLAUDE_CONFIG_DIR=$HOME/.claude-work when launched under ~/Git/work
(symlinks resolved on both sides); a pre-set CLAUDE_CONFIG_DIR always wins;claude --personal forces the personal account. claude-account prints the
resolution; claude/hooks/account_guard.py (SessionStart) warns on a
dir/account mismatch. Unwrapped launches (desktop app) default to ~/.claude,
which is why personal is the default dir, not work.
Invariant: any new Claude asset goes into claude/ and gets linked bylink_claude_config_dir -- never hand-placed into one config dir.
Decision 3: fail-loud git identity
git/.gitconfig has NO [user] block. Instead:
user.useConfigOnly = true-- git refuses to commit anywhere it cannot
resolve an identity, instead of guessing one. Wrong-identity commits on a
public repo are unrecallable; a loud failure is the feature.includeIf "gitdir/i:~/Git/personal/"->~/.gitconfig-personal(tracked);includeIf "gitdir/i:~/Git/work/"->~/.gitconfig-work(seeded, never
tracked).gitdir/iis case-insensitive for APFS; linked worktrees resolve
to the main repo's gitdir, so worktrees inherit identity wherever they live.- Signing lives INSIDE each include (each pairs
gpgsign=truewith its ownsigningkey), never at base level -- a basegpgsign=truewould break the
repo-localuser.emailescape hatch for repos outside the convention dirs
(rationale comment ingit/.gitconfig,[commit]block). url insteadOfin each include rewritesgit@github.com:to the pinned SSH
alias (Git-Personal/Git-work), so fresh clones authenticate with the
right key with zero manual steps;pushInsteadOfkeeps https fetch anonymous
but pushes over SSH.
Verify the whole chain read-only: git identity (alias forbin/identity-doctor). Populate work identity: bin/identity-setup.
Invariant: identity comes only from the conditional includes. Never add a[user] block to git/.gitconfig; never commit ~/.gitconfig-work contents.
Decision 4: vendored-but-untracked upstream content (historical; ECC retired)
This decision was born from the retired ECC: its skills/commands/hooks lived
on disk inside the repo tree but were gitignored, and installers re-vendored
them byte-identical from upstream. Why: tracking them once caused 39-file
churn on every upstream sync (fixed at e140ab3). They were reproducible from
the installer, so tracking added noise and invited local edits that died on
the next ecc-update (both ecc-install/ecc-update are now removed along with the retired ECC itself, 2026-09).
ECC RULES vendoring was fully RETIRED ahead of ECC itself (2026-07-02,
settling the former open question): the upstream tree shipped from the retired ECC's marketplace clone, so local copies bought sync code for nothing. Claude Code
natively auto-loads every .md under ~/.claude/rules (paths: frontmatter
scopes to matching files; none = every session) -- verified live in a cloud
session 2026-07-02. The retirement-era "nothing auto-loads" finding came from
fresh cloud clones where the untracked vendored dirs did not exist; leftover
language dirs on older machines therefore still auto-load and should be
deleted by hand (the flagging function, _ecc_legacy_rules_notice, is gone along with the rest of the retired ECC).
Invariant: never hand-edit any leftover vendored dirs; never git add -f
them. Own rules go under claude/rules/personal/ only (always-on unless
given a paths: frontmatter; claude/rules/.gitignore whitelists only.gitignore and personal/).
Decision 5: the pre-launch placement INVARIANT (cloud plugins)
Anything that must exist at Claude Code launch must be placed pre-launch.
Post-launch hooks are self-heal only.
Plugins load when the CLI starts. A SessionStart hook runs AFTER launch, so a
plugin it installs is invisible until the next session (root cause: c1c4500,
after four earlier fixes chased symptoms -- see dotfiles-failure-archaeology).
Final design (8d4507f): the repo's committed .claude/settings.json declaresenabledPlugins and pins both marketplaces by git URL inextraKnownMarketplaces; the platform installs them natively pre-launch, so
they work on session 1. .claude/hooks/session-start.sh (gated onCLAUDE_CODE_REMOTE=true, matcher startup|resume) runs bootstrap-cloud.sh
as the post-launch SELF-HEAL: assets, settings reconcile, git identity, and a
plugin re-install that only pays off next session if the native path missed.
Corollaries baked into bootstrap-cloud.sh, keep them:
claude plugin installcan exit 0 without installing;ensure_plugin
verifies against~/.claude/plugins/installed_plugins.jsoninstead
(f91d7d2).- GitHub-backed marketplaces register async; wait for the manifest before
installing (7cb28b7), with backoff capped at 15s so the platform Setup
script's ~5-minute budget is never blown (da54e17). - Cloud containers seed commits as
Claude <noreply@anthropic.com>;
bootstrap-cloud reattributes to the personal identity and disables signing
(no 1Password in a container) -- never copy the op-ssh-sign block into a
container (2304015).
Decision 6: one global git hooks dir for all harnesses
core.hooksPath = ~/.config/git/hooks (git/.gitconfig), a symlink to this
repo's git/hooks/ (created by install/common/link.sh). One dir serves
Claude sessions, Codex sessions, and manual commits -- git hooks are
harness-agnostic, so guard logic (commit-msg format, secret blocking,
worktree hydration via post-checkout) applies no matter who commits.
Why it is fragile by design: anything that rewrites core.hooksPath orphans
ALL of it silently. That is exactly what the retired ECC's sync-ecc-to-codex.sh did pre-public (plus writing through the~/.codex/AGENTS.md symlink into the repo). Both direct sync and its
wrapper are retired along with ECC itself (2026-09), and the retired Superpowers install path never used any upstream global sync either.install/common/link.sh sweeps leftover mirror artifacts on every update.
Invariant: no tool may set core.hooksPath; hook additions go intogit/hooks/ with a test.
Decision 7: idempotency contract
The update() zsh function (zsh/functions.zsh) pulls the repo and re-runsinstall/install.sh in full. There is no "first install" vs "update" code
path -- the installer IS the updater. Therefore every install step must be
safely re-runnable on a machine that already has the state: ln -sfn notln -s, seed-if-absent not overwrite, backup-then-replace for real dirs where
a symlink belongs (link_claude_config_dir does this forcommands/agents/hooks/rules/skills), sweeps for retired artifacts.
Invariant: before merging any installer change, ask "what happens on the
second run, and on a machine with pre-existing real files where symlinks
belong?" If the answer involves data loss or a duplicate, it is not done.
Decision 8: the public-repo constraint shapes everything
github.com/taloncjones/dotfiles is public. Consequences already visible above:
- Employer identity, vault names, machine-local hosts live ONLY in seeded
files (~/.gitconfig-work,~/.ssh/config_local,agent.toml) -- Decision 1. - Only PUBLIC key halves are tracked (
ssh/keys/id_ed25519_personal.pub);
private keys never touch disk (1Password agent). - Wrong-identity commits are prevented structurally, not by discipline --
Decision 3. - Guard hooks +
git/hooks/public-safety.test.sh+ CI backstop it, but the
architecture is arranged so the secret has no natural path into the tree in
the first place. The gates are the last line, not the design.
Invariant: a new feature that needs private data must put that data in a
seeded machine-local file (or env var), with a tracked template documenting
its shape.
Invariants checklist
Before approving a structural change, confirm each still holds:
- No runtime-written or private-content file is symlinked into the repo
(settings.json, .gitconfig-work, config_local, agent.toml stay seeded). - All Claude assets flow through
claude/+link_claude_config_dir;~/.claudeand~/.claude-worknever diverge in assets. -
git/.gitconfighas no[user]block;useConfigOnlystays true;
signing stays inside the per-identity includes. - Any leftover vendored dirs from the retired ECC stay untracked and
hand-edit-free; own content stays in whitelistedpersonal/paths. - Anything needed at Claude launch in cloud is placed pre-launch
(settings declaration or Setup script); SessionStart remains self-heal. -
core.hooksPathpoints at the dotfiles hooks dir and nothing rewrites it. - Every installer step is idempotent (safe twice, safe over pre-existing
state). - No secret, employer datum, or machine path can land in the tree by any
normal workflow.
Known weak points (open -- stated plainly, not oversold)
| Weak point | Status |
|---|---|
claude/settings.json.tmpl changes need MANUAL merge on existing machines (seed-once); only cloud runs reconcile_claude_settings automatically |
Open. Accepted cost of Decision 1; change-control skill has the merge protocol |
Machine-path plugin install weak point (retired): the retired Superpowers and ECC install paths trusted CLI output (grep of plugins list), not installed_plugins.json; the ensure_plugin gold standard they never adopted is deleted too |
Closed by retirement: no plugin install path remains to carry the weak point |
Git hooks pre-commit/pre-push (derived from the retired ECC) have behavioral suites; post-checkout test remains static-shape-only |
Partially closed; see dotfiles-validation-and-qa |
op-ssh-sign path is macOS-only -- commit signing fails on Linux (bootstrap-cloud disables signing in containers); install/common/zsh.sh hardcodes the /home/linuxbrew path; defaults.sh PlistBuddy Set lacks an Add fallback |
Open portability debts |
Cloud containers are personal-account only; no ~/.claude-work story in cloud |
Open |
| Whether ECC rules vendoring is still needed at all | CLOSED: ECC is fully retired (2026-09), question moot (Decision 4) |
When NOT to use this skill
- Making, committing, or reviewing an actual change (gates, classification,
commit format) -> dotfiles-change-control. - Something is broken and you need triage -> dotfiles-debugging-playbook.
- You want the full incident narrative behind a decision (PR-by-PR, dead ends,
reverts) -> dotfiles-failure-archaeology. - Claude Code platform mechanics in general (config-dir semantics, plugin/
marketplace internals, hook lifecycle) -> claude-code-platform-reference. - Recreating a machine or container from scratch -> dotfiles-build-and-env.
- Proving an invariant currently holds on a live machine (symlink chains,
plugin installs, identity resolution) -> dotfiles-verification-toolkit.
Provenance and maintenance
Written 2026-07-02 against the repo at that date. All commit hashes verified
with git log --format="%h %s" -n1 <hash> this day. Re-verify volatile facts
before trusting them:
- Symlink/seed behavior:
grep -n "seed_machine_local_file\|ln -sfn" install/common/claude-links.sh - Account routing:
grep -n "_claude_config_dir\|CLAUDE_WORK_TREE" zsh/claude-account.zsh - Identity chain:
grep -n "useConfigOnly\|includeIf\|hooksPath" git/.gitconfigand runbin/identity-doctor - Vendored-untracked model:
cat claude/rules/.gitignore - Pre-launch plugin declaration:
cat .claude/settings.json(enabledPlugins + extraKnownMarketplaces); self-heal gate:head -25 .claude/hooks/session-start.sh - ensure_plugin / reconcile:
grep -n "ensure_plugin\|reconcile_claude_settings" bootstrap-cloud.sh - update() re-run contract:
grep -n -A8 "function update()" zsh/functions.zsh - Incident hashes:
git show <hash> --statfor c1c4500, 8d4507f, 722c653, 7cb28b7, da54e17, 2304015, e140ab3 - Weak-point status may have improved since 2026-07-02 -- check
git log --oneline -20and the failure-archaeology skill before repeating an "open" claim.
Install layout reference
references/install-layout.md holds the install flow, every symlink and
seed target, the hook catalogue, and the Codex plugin and Herdr notes that
used to live in the repo CLAUDE.md. Read it before adding a symlink target,
a hook, or an installer step, and update it in the same change.
Codex plugin integration (reference; ECC retired 2026-09, Superpowers retired 2026-09)
Superpowers (retired) and ECC (retired) used the same native, independent plugin shape in both runtimes: Claude installed a claude-plugins-official id in both account config dirs (superpowers@claude-plugins-official, ecc@ecc); Codex staged a self-contained copy under ~/.local/share/dotfiles/codex-workflows (superpowers@dotfiles-workflows, ecc@dotfiles-workflows). Both are retired: the settings reconcile pins the Claude copy off while it is still registered, and the Codex plugin dedupe forces every Codex copy off, and ecc-uninstall/superpowers-uninstall remove what remains on disk; the Claude registration itself is removed on every update by sweep_retired_claude_plugins, which both uninstallers also call. Never run the retired ECC's sync-ecc-to-codex.sh on a dotfiles-managed machine: it mutated shared AGENTS.md, MCP, agent, and git-hook surfaces. install/common/link.sh continues to sweep stale direct skill/agent mirrors from older installs.
install/common/codex-surfaces.py reconciles native discovery during install
and update. The surviving legacy-copy/legacy-command blocks it wrote for
the retired ECC's standalone skill copies are kept disabled verbatim; nothing
regenerates them. It also disables incompatible or inert Claude-only imports
of security-guidance, code-review, and code-simplifier; custom or
compatible implementations are preserved. It
supplies skills.max_context_tokens = 10000 only when unset; an explicit
budget wins. The helper preserves skill files and unrelated configuration.
Unsupported TOML layouts fail without being rewritten. Optional installer
reconciliation warns and continues; explicit plugin lifecycle commands still
report failure. Managed skill linking preserves existing real files and
directories, including their contents, and warns instead of replacing them;
inspect those destinations before moving custom content to make room for a
managed symlink.
Shared handoff, kickoff, todos, repo-recall, and post-merge skills
link from claude/skills into Codex. Codex review and Herd entrypoints live incodex/skills. Helpers resolve paths from these installed sources, not the
repository being worked on. workflow_context.py owns repository/account
identity; handoff history is account/repository/task scoped. Never copy a
personal handoff into a work account's state.
install/common/codex-roles.py migrates only byte-exact historical managed
roles left over from the retired ECC (identified by the SHA256 of its.codex/agents files) to Luna/medium explorer, Terra/medium documentation
research, and Astra/high reviewer. Custom role files and model choices
remain untouched; --check --codex-home <path> previews its decision.
Repo-owned codex/AGENTS.md does not carry a copied upstream instruction
block from any plugin.
Plugin staging records upstream revision and payload digest independently of
the wrapper version. A now-retired Codex ECC cache directory named 2.0.0
could contain a current upstream payload; verify installed bytes/provenance
before declaring anything stale. Automatic Plan Canvas session/stop hooks (an ECC-only feature) are gone along with the rest of the retired ECC.
Run the helper with --check to preview changes or --apply to write them.
The old --focus --apply flag, which opted into the now-retired codex/ecc-skills.txt core skill catalog, no longer exists; there is no
focused-discovery mode. Explicit skill overrides are preserved.
Leftover [[skills.config]] blocks marked with the now-retired # dotfiles-managed: ecc-focus tag are cleared automatically byclear_managed_disabled on every
repair run; there is no manual removal step. The legacy-copy/legacy-command
blocks remain disabled. The compatibility checks run again on later updates;
simply re-enabling an incompatible import does not opt it out.
Restart Claude or Codex after changing plugin or hook configuration. A running
session may retain old registrations, including across compaction. Verify a
fresh Codex process with a harmless tool call and final response: startup alone
does not exercise PostToolUse or Stop.