Install artifacts (skills, rules, agents, commands) into coding agents
Project description
๐ loadout
The package manager for AI coding agent artifacts.
Stop manually copying skills, rules, agents, and commands into every coding agent's config directory. loadout handles discovery, transformation, and installation across Claude Code, Cursor, and OpenCode โ so your CLI or project setup script doesn't have to.
๐ฆ Installation
# uv (recommended)
uv add loadout
# pip
pip install loadout
# With interactive agent selection prompt
uv add "loadout[interactive]"
pip install "loadout[interactive]"
๐ค Why loadout?
Every team building on top of AI coding agents hits the same problem: distributing custom skills, rules, and commands to developers' machines. The config directories differ, the file formats differ, and the logic for "install this skill to that agent" gets copy-pasted across tooling.
loadout extracts that logic into a single, typed, tested library with an extensible adapter pattern. You write your artifacts once. loadout puts them where they belong.
- ๐ Agent-agnostic โ one artifact definition works across all supported agents
- ๐ Auto-detection โ discovers which agents are installed on the system
- ๐ Convention + configuration โ scan for marker files, or define a manifest
- ๐ Adapter pattern โ add support for new agents without touching core logic
- ๐ช Lifecycle hooks โ plug in your own logging, progress bars, or analytics
- ๐ก๏ธ Fully typed โ strict mypy, Pydantic models, protocol classes
โก Quick start
๐ Install everything to every detected agent
from loadout import install_all
summary = install_all("./my-artifacts", force=True)
print(f"โ
Installed: {len(summary.installed)}")
print(f"โญ๏ธ Skipped: {len(summary.skipped)}")
print(f"โ Failed: {len(summary.failed)}")
๐ฌ Interactive mode (checkbox prompt)
uv add "loadout[interactive]"
from loadout import install_interactive
summary = install_interactive("./my-artifacts")
๐๏ธ Full control
from loadout import discover_artifacts, detect_agents, install
artifacts = discover_artifacts("./my-artifacts")
agents = detect_agents()
summary = install(artifacts, agents, force=True)
Three tiers โ pick the one that fits your UX. ๐ฏ
๐ค Supported agents
| Agent | Config dir | Skills | Rules | Agents | Commands |
|---|---|---|---|---|---|
| Claude Code | ~/.claude/ |
โ | โ | โ | โ |
| Cursor | ~/.cursor/ |
โ | โ (.mdc) | โ | โ |
| OpenCode | ~/.opencode/ |
โ | โ | โ | โ |
loadout auto-detects which agents are present and only installs to agents that support each artifact type. Unsupported combinations are cleanly skipped. โจ
๐ Artifact discovery
๐ Convention-based (marker files)
Drop marker files into your artifact directories and loadout will find them:
my-artifacts/
login-skill/
SKILL.md # โ marker: this directory is a skill
helper.py
utils.py
security/
auth-rule/
RULE.md # โ marker: this file is a rule
setup-agent/
AGENT.md # โ marker: this file is an agent
deploy/
COMMAND.md # โ marker: this file is a command
Marker files double as the artifact content โ the SKILL.md is the skill. Categories are derived from directory structure (security/auth-rule/ โ category security).
๐ Manifest-based (loadout.yaml)
For explicit control, add a loadout.yaml to the root of your artifacts directory:
artifacts:
- name: login-skill
type: skill
path: login-skill
- name: auth-rule
type: rule
path: security/auth-rule/RULE.md
category: security
description: "Enforces authentication checks"
When a manifest is present, marker-file scanning is skipped entirely โ you have full control over what gets installed.
๐ Frontmatter support
Artifact files can include YAML frontmatter for metadata:
---
description: Handles user login flows
globs:
- "src/auth/**"
always_apply: true
---
# Login Skill
Your skill content here...
๐ Custom adapters
Need to support a new coding agent? Implement the AgentAdapter interface and register it:
from pathlib import Path
from loadout import (
AgentAdapter,
Artifact,
ArtifactType,
DetectedAgent,
InstallResult,
get_default_registry,
install_all,
)
class WindsurfAdapter(AgentAdapter):
@property
def agent_name(self) -> str:
return "windsurf"
@property
def display_name(self) -> str:
return "Windsurf"
@property
def config_dir_name(self) -> str:
return ".windsurf"
def supported_artifact_types(self) -> set[ArtifactType]:
return {ArtifactType.SKILL, ArtifactType.RULE}
def detect(self) -> DetectedAgent | None:
config_dir = Path.home() / self.config_dir_name
if config_dir.is_dir():
return DetectedAgent(
name=self.agent_name,
config_dir=config_dir,
display_name=self.display_name,
)
return None
def get_target_path(self, artifact: Artifact, config_dir: Path) -> Path:
# Your path resolution logic
...
def transform_content(self, artifact: Artifact, content: str) -> str:
# Your content transformation logic
return content
def transform_filename(self, artifact: Artifact, filename: str) -> str:
return filename
def install(self, artifact: Artifact, agent: DetectedAgent, force: bool = False) -> InstallResult:
# Your install logic
...
# Register and use ๐
registry = get_default_registry()
registry.register(WindsurfAdapter())
summary = install_all("./my-artifacts", registry=registry)
The adapter pattern means core loadout never needs to change when new agents appear. ๐งฉ
๐ช Lifecycle callbacks
Hook into every stage of the installation process for logging, progress bars, analytics, or custom error handling:
from loadout import LoadoutCallbacks, Artifact, DetectedAgent, InstallResult, install_all
class RichCallbacks:
"""Example: pretty-print progress with Rich."""
def on_artifact_discovered(self, artifact: Artifact) -> None:
print(f" ๐ Found {artifact.artifact_type.value}: {artifact.name}")
def on_agent_detected(self, agent: DetectedAgent) -> None:
print(f" ๐ค Detected agent: {agent.display_name}")
def on_install_started(self, artifact: Artifact, agent: DetectedAgent) -> None:
print(f" โณ Installing {artifact.name} โ {agent.display_name}...")
def on_install_complete(self, result: InstallResult) -> None:
print(f" โ
Installed to {result.target_path}")
def on_install_skipped(self, result: InstallResult) -> None:
print(f" โญ๏ธ Skipped: {result.error}")
def on_install_failed(self, result: InstallResult) -> None:
print(f" ๐ฅ FAILED: {result.error}")
summary = install_all("./my-artifacts", callbacks=RichCallbacks())
Only override the hooks you care about โ the LoadoutCallbacks protocol defines the full interface, and NoOpCallbacks provides a ready-made base with no-op defaults.
๐ API reference
โ๏ธ Top-level functions
| Function | Description |
|---|---|
install_all(source_dir, force, registry, callbacks) |
Discover artifacts, detect agents, install everything |
install_interactive(source_dir, force, registry, callbacks) |
Same as above with interactive agent selection |
install(artifacts, agents, force, registry, callbacks) |
Install specific artifacts to specific agents |
discover_artifacts(source_dir) |
Scan a directory and return a list of Artifact objects |
detect_agents(registry) |
Detect installed coding agents |
get_default_registry() |
Get the built-in adapter registry |
๐งฑ Models
| Model | Description |
|---|---|
Artifact |
A discovered artifact (name, type, source path, category, frontmatter) |
ArtifactType |
Enum: SKILL, RULE, AGENT, COMMAND |
DetectedAgent |
An agent found on the system (name, config dir, display name) |
InstallResult |
Result of a single artifact install (status, target path, error) |
InstallSummary |
Batch result with .installed, .skipped, .failed, .already_existed |
Manifest |
Parsed loadout.yaml manifest |
๐จ Exceptions
| Exception | Description |
|---|---|
LoadoutError |
Base exception for all loadout errors |
ArtifactNotFoundError |
Source artifact path does not exist |
ManifestError |
Invalid loadout.yaml |
InstallError |
Installation failed |
AdapterNotFoundError |
No adapter registered for the given agent |
AdapterAlreadyRegisteredError |
Adapter name collision |
TransformError |
Content transformation failed |
๐ ๏ธ Development
# Clone and install with all extras
git clone https://github.com/nickmaccarthy/loadout.git
cd loadout
# uv (recommended)
uv sync --all-extras
# pip
pip install -e ".[dev,interactive]"
# Set up pre-commit hooks
uv run pre-commit install
# Run all pre-commit checks (ruff, mypy, formatting, etc.)
uv run pre-commit run --all-files
# Run tests
uv run pytest
# Run tests with coverage
uv run pytest --cov=loadout --cov-report=term-missing
๐ก Pre-commit hooks run automatically on every
git commit, catching lint errors, type issues, and formatting problems before they hit CI.
๐ Requirements
- ๐ Python 3.10+
- pydantic >= 2.0
- PyYAML >= 6.0
- questionary >= 2.0 (optional, for interactive mode)
๐ License
MIT โ see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file loadout-0.4.0.tar.gz.
File metadata
- Download URL: loadout-0.4.0.tar.gz
- Upload date:
- Size: 19.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8cbc25f4f748ec5616549476f3fac551c8c7a73ba080c808c5c1a84d57f33bc4
|
|
| MD5 |
e6458e69c1815201471edb3814ed68c8
|
|
| BLAKE2b-256 |
178ce333024ee5349b27aba084a955a9109d8b16a2eda44b9b30b6abb5d0bad2
|
Provenance
The following attestation bundles were made for loadout-0.4.0.tar.gz:
Publisher:
release.yml on nickmaccarthy/loadout
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
loadout-0.4.0.tar.gz -
Subject digest:
8cbc25f4f748ec5616549476f3fac551c8c7a73ba080c808c5c1a84d57f33bc4 - Sigstore transparency entry: 1093963917
- Sigstore integration time:
-
Permalink:
nickmaccarthy/loadout@03bac990d5c637b81010e81dedd2342ebc1b47eb -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nickmaccarthy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@03bac990d5c637b81010e81dedd2342ebc1b47eb -
Trigger Event:
push
-
Statement type:
File details
Details for the file loadout-0.4.0-py3-none-any.whl.
File metadata
- Download URL: loadout-0.4.0-py3-none-any.whl
- Upload date:
- Size: 18.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
037e129f733d99fe7e7cebc9e3b3003f8ac11e7d6371723dd523a3f58030eaf0
|
|
| MD5 |
b20dee9685e6422c494949fca4890a70
|
|
| BLAKE2b-256 |
c98acdb7ef919e2fa9aae5cc54552ab4b0563a2a6205592e145d9ee5b68280a1
|
Provenance
The following attestation bundles were made for loadout-0.4.0-py3-none-any.whl:
Publisher:
release.yml on nickmaccarthy/loadout
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
loadout-0.4.0-py3-none-any.whl -
Subject digest:
037e129f733d99fe7e7cebc9e3b3003f8ac11e7d6371723dd523a3f58030eaf0 - Sigstore transparency entry: 1093963919
- Sigstore integration time:
-
Permalink:
nickmaccarthy/loadout@03bac990d5c637b81010e81dedd2342ebc1b47eb -
Branch / Tag:
refs/heads/main - Owner: https://github.com/nickmaccarthy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@03bac990d5c637b81010e81dedd2342ebc1b47eb -
Trigger Event:
push
-
Statement type: