Loadout is a Go CLI/TUI that manages machine-local installation of agent skills for Claude and Codex from a shared git-backed skills repository.
The architecture is intentionally simple:
- The shared repo stores skill content and metadata only.
- Each machine stores its own configuration locally.
- Installation is copy-based, not symlink-based.
- Installed skill directories are disposable derived artifacts.
- CLI and TUI both delegate business operations to
internal/app.Service. - Import preserves authored target declarations by default and does not expand support automatically.
This document describes the current implementation for contributors working in this codebase.
Loadout operates across three storage domains:
- Skill source repository
A git repository containing a
skills/tree. Each skill lives in its own directory withskill.jsonmetadata andSKILL.mdcontent. - Local configuration
Machine-local configuration stored in
~/.config/loadout/config.json. This defines the source repo path and the target install roots for Claude and Codex. - Installed target directories
Derived copies of skills stored under target roots such as
~/.claude/skillsand~/.codex/skills, or under project roots in project mode.
The source repo is the only shared input. Everything else is local machine state.
- The skills repo contains content only. It does not store machine state.
- Loadout installs by copying directories into target roots.
- Removal deletes installed copies; there are no symlinks or in-place references back to the source repo.
- Installed copies can always be regenerated from the repo.
- The TUI is presentation and interaction state only. It does not implement business rules.
internal/domainremains dependency-free and owns core types, validation, and sentinel errors.
Dependency direction:
cmd -> app, tuitui -> app, domainapp -> domain, config, registry, gitrepo, install, importer, reconcile, scopedomain -> nothing
cmd/loadout/main.gostarts Cobra.cmd/loadout/cmdcontains CLI commands such asinit,inventory,equip,unequip,import,delete,sync, anddoctor.- Running
loadoutwith no subcommand starts the Bubble Tea TUI.
internal/app is the orchestration boundary for both CLI and TUI.
app.Service is the central application API. It loads skills, previews content, equips and unequips targets, syncs the repo, runs health checks, and supports project installs. CLI commands and TUI commands call into the service instead of reaching into lower-level packages directly.
It also owns repo mutation workflows such as importing a local skill directory into the shared repo and discovering unmanaged local skills for import in the TUI.
internal/domain owns pure types and validation:
SkillNameidentifies a skill.Skillmodels repo metadata loaded fromskill.json.Targetidentifies supported install targets such as Claude and Codex.- Validation ensures skill metadata is structurally valid before the rest of the system acts on it.
- Sentinel errors:
ErrSkillNotFound,ErrSkillInstalled,ErrInvalidSkill,ErrSkillExists,ErrImportConflict,ErrUnsupportedTarget,ErrTargetDisabled,ErrRepoNotFound,ErrConfigNotFound.
internal/config loads and saves machine-local configuration at ~/.config/loadout/config.json.
The config currently stores:
repo_path- per-target
enabledflags and target roots for Claude and Codex repo_actions.import_auto_commitandrepo_actions.delete_auto_commit(default true)
The config is the source of truth for user install roots.
internal/registry loads the skill catalog from the configured repo path.
Responsibilities:
- scan
skills/ - require both
skill.jsonandSKILL.md - unmarshal metadata into
domain.Skill - validate loaded skills
- detect duplicate names
- expose full catalog loading, single-skill lookup, and markdown preview loading
The registry layer treats the git repo as read-only content.
internal/importer owns repo-facing skill import behavior.
Responsibilities:
- inspect an existing local skill directory
- preserve declared
skill.jsontargets and target-specific metadata as-authored - infer minimal metadata when
skill.jsonis absent - strip install-only frontmatter from imported
SKILL.md - remove
.loadoutmarkers from imported repo copies - discover unmanaged local candidates from enabled target roots
- detect conflicting duplicate local copies across Claude and Codex roots
Import is intentionally conservative. If two local copies disagree on authored metadata or target declarations, Loadout surfaces that as a conflict instead of merging them automatically.
internal/skillmd parses YAML frontmatter and headings from SKILL.md files. The importer uses this to infer skill name and description when skill.json is absent. It extracts the first heading as a candidate name and any frontmatter fields that carry metadata.
internal/gitrepo wraps git operations:
- repo detection
- fast-forward pull
- current HEAD commit lookup
- dirty working tree checks
- repo initialization and clone
- sync-readiness checks for tracked branches
This package is intentionally small and shell-command based.
Import uses a scoped staging helper that commits only the imported skill path when auto-commit is enabled.
internal/install owns copy/remove behavior in target roots.
Install behavior:
- verify target support
- copy the skill directory into a temp dir on the target filesystem
- transform
SKILL.mdby prepending generated YAML frontmatter - write the
.loadoutmarker into the staging directory - atomically rename into place
Remove behavior deletes the installed directory if present.
The .loadout marker is how Loadout distinguishes managed installs from unmanaged directories that happen to exist under the same target root.
The generated frontmatter is a derived install artifact. Repo copies of SKILL.md remain plain markdown without the prepended frontmatter block.
internal/reconcile compares desired catalog state with actual installed state.
Today it does not emit imperative operations. Instead, it computes per-skill status used by inventory views and health reporting. It distinguishes:
- installed vs not installed
- managed vs unmanaged
- present in repo vs missing from repo
This is the package that turns filesystem observations into higher-level inventory status flags.
internal/tui contains Bubble Tea presentation logic:
- screen state
- key handling
- inventory filtering
- details pane rendering
- project/user mode toggles
- settings screen state
- import screen state
All business actions are delegated through tea.Cmd helpers that call app.Service.
internal/scope resolves whether a command or TUI session is operating in user scope or against a project root.
In project mode, the effective install roots become:
<project>/.claude/skills<project>/.codex/skills
Project mode is used by CLI commands that accept --project and by TUI startup when a compatible project root is detected.
domain.Skill is loaded from skill.json and includes:
- name, description, and tags
- supported targets
- optional target-specific metadata maps for Claude and Codex
- an internal
Pathfield pointing back to the skill directory in the repo
skill.json is the metadata source of truth. SKILL.md is the source markdown body.
config.Config is persisted at ~/.config/loadout/config.json and currently contains:
repo_pathtargets.claude.enabledtargets.claude.pathtargets.codex.enabledtargets.codex.path
This file defines where Loadout reads source content from and where user installs are written.
Each managed install includes a .loadout JSON file with:
repo_commitinstalled_at
Current on-disk shape:
{
"repo_commit": "abc1234",
"installed_at": "2026-03-21T14:22:33Z"
}This marker supports two important behaviors:
- detect whether an installed skill directory is managed by Loadout
- determine whether a managed install is behind the repo's current HEAD and must be refreshed during sync
Loadout makes a deliberate distinction between "installed" and "managed":
- Installed means a skill directory exists under a target root.
- Managed means that installed directory also contains a
.loadoutmarker.
This matters because target roots may already contain directories not created by Loadout. The app does not assume every directory is under its control.
app.Service.scanActual collects both facts:
- presence of any skill-named directory
- presence of a
.loadoutmarker for that directory
reconcile.Plan then turns that into inventory flags such as inactive, current, unmanaged, or missing-from-repo.
The import discovery flow depends on the same distinction:
- managed directories contain
.loadoutand are excluded from import candidates - unmanaged directories under enabled target roots are potential import candidates
User inventory flow:
- CLI or TUI calls
Service.ListSkills. - The service loads the registry from the configured repo path.
- The service scans target roots for actual installed directories and
.loadoutmarkers. reconcile.Plancompares registry skills against actual state.- The result is returned as
app.SkillViewvalues for CLI output or TUI rendering.
Project inventory flow:
- CLI resolves
--projector TUI switches into detected project mode. Service.ListSkillsForProjectloads the same registry inventory used by user mode.- The service scans both user target roots and the project target roots.
- The UI renders project install state as the active scope while still showing user installs as informational context.
- CLI inspect or TUI selection requests a preview.
Service.PreviewSkillloads one skill from the registry.- The service reads the source
SKILL.mdfrom the repo. - The preview returns metadata plus raw markdown content.
Preview reads source content, not the transformed installed copy.
- CLI
importor the TUI import screen callsService.ImportPath. - The service validates that the configured repo is a git repo.
internal/importerinspects the source directory and normalizes it into repo format.- The importer preserves declared targets from
skill.jsonexactly as-authored. - If
skill.jsonis missing, the importer infers metadata from frontmatter, headings, and explicit or inferred source targets. - The importer copies the directory into
repo/skills/<id>/, strips frontmatter fromSKILL.md, removes.loadout, and writes normalizedskill.json. - If auto-commit is enabled, the service stages only that imported skill path and creates a commit.
- The TUI import screen calls
Service.ListImportCandidates. - The service asks
internal/importerto scan enabled target roots only. - Candidate discovery skips managed directories that already contain
.loadout. - If the same normalized skill name appears in both Claude and Codex roots:
- identical normalized copies are shown as one duplicate candidate
- conflicting copies are shown as a blocked candidate with a problem message
Import discovery is a convenience wrapper around import, not a separate business workflow.
- CLI or TUI calls
Service.DeleteSkillEligibilityto check whether deletion is safe. - The service verifies the skill exists in the registry and checks for managed installs that would block deletion.
- If eligible,
Service.DeleteSkillremoves the skill directory from the repo'sskills/tree. - If
repo_actions.delete_auto_commitis enabled, the service stages the removal and creates a commit.
Deletion is blocked when managed installs (user or project) still reference the skill. Unmanaged copies do not block deletion.
User equip:
- CLI or TUI calls
EnableSkillTargetorToggleSkillTarget. - The service loads the requested skill from the registry.
- The service verifies the skill supports the requested target.
- The service resolves the configured target root.
install.Installcopies the repo content, transformsSKILL.md, writes the.loadoutmarker into staging, and atomically renames the completed directory into place.
User unequip:
- CLI or TUI calls
DisableSkillTargetorToggleSkillTarget. - The service resolves the configured target root.
install.Removedeletes the installed directory if present.
Project-local equip and unequip follow the same install/remove path, but target roots are derived from the project root instead of user config.
- CLI or TUI calls
Service.SyncRepoWithResult. - The service verifies the configured repo is a git repo.
- Best-effort startup checks may already have highlighted sync attention by combining:
repo dirtiness,
remote-behind status from
git fetch, and managed-install drift from.loadout.repo_commitversus local HEAD. gitrepo.Pullperformsgit pull --ff-only.- The service reloads the registry and resolves the new local repo HEAD.
- For each relevant target root,
install.ScanManagedfinds managed installs. Relevant means user roots always, plus the active project roots when sync runs in project scope. - Each managed install's
.loadout.repo_commitis compared to the current repo HEAD. - Managed installs still present in the registry, still supporting that target, and behind HEAD are reinstalled in place from repo content.
Sync is now the explicit apply step for managed-install freshness. It refreshes outdated managed copies automatically after the repo is updated, while leaving unmanaged directories untouched.
- CLI or TUI calls
Service.Doctor. - The service checks whether the configured repo exists and looks like a git repo.
- The service checks sync readiness for the current tracked branch.
- The service attempts to load the registry.
- The service reports configured target roots.
- If the registry loaded successfully, the service scans managed installs and reports whether any are missing from the registry.
Doctor is a read-only health report. It does not attempt repair.
- Root command creates
app.Servicefrom local config. - The root command detects a project root from CWD and passes it to the TUI. When a project is detected the TUI starts in project scope; otherwise it starts in user scope. The
--userflag skips project detection and forces user scope. - The TUI model loads skills on startup for the active scope.
- Key actions map to
tea.Cmdhelpers ininternal/tui/commands.go. - Those commands call into
app.Service. - Result messages update the model and refresh the current scope view as needed.
This keeps business rules out of update/render code and makes the TUI a thin stateful shell around the service layer.
Loadout supports two installation scopes:
- User mode writes to paths from
config.Config. - Project mode writes under a detected project root.
Project mode is resolved through internal/scope:
Resolvehandles--projectDetectProjectRootwalks upward to a git root and requires either.claude/or.codex/
This allows a contributor to manage repository-specific agent skill directories without changing user configuration.
The TUI defaults to project mode at startup when a compatible project is detected, and shows a one-time status cue confirming the active scope. The tab key toggles between scopes during a session.
In the TUI, project mode reuses the full registry-backed inventory rather than switching to an installed-only project list. The selected scope changes:
- which install roots
c,x, andaoperate on - which install status is rendered as primary
- which utility hints are shown, including project import hints
User installs remain visible in project mode as read-only context so the user can choose to add a project copy without first removing the user install.
The codebase tests architecture-critical behavior using stdlib-only, table-driven tests.
Notable coverage areas:
- metadata validation in
internal/domain - registry loading and invalid fixture handling
- filesystem copy/install behavior with
t.TempDir() - reconcile status computation
- service-level orchestration including doctor, sync error paths, and project flows
- command-level initialization flow
Filesystem behavior is tested against real temp directories rather than mocks. That matches the app's design, where filesystem state is part of the core behavior.
When changing the system:
- Add or change business workflows in
internal/appfirst, then expose them through CLI/TUI. - Keep UI concerns in
internal/tui; do not move install, registry, or policy logic there. - Extend
domain.Skillonly when the new field belongs in repo metadata. - Preserve marker-based managed install detection unless intentionally redesigning ownership semantics.
- Preserve copy-based installation and target-specific
SKILL.mdtransformation behavior. - Prefer concrete types and narrow package APIs over broad shared interfaces.
Good extension points include:
- adding a new CLI command that calls into
app.Service - expanding doctor or inventory reporting
- supporting additional target-specific metadata fields
- adding more project-mode workflows
Changes that should be treated carefully:
- altering target root semantics
- changing
.loadoutmarker format - moving business logic into TUI update/view code
- making the repo responsible for local machine state
The current architecture deliberately keeps state light.
- There is no separate persistent machine-state database beyond config and install markers.
- Reconcile computes status, not executable convergence operations.
- Sync refreshes managed installs only; it does not attempt to reconcile unmanaged directories.
- Preview reads source markdown, not the installed transformed copy.
These are current implementation facts, not accidental omissions.