-
-
Notifications
You must be signed in to change notification settings - Fork 2
Bundles
Coyote ships with a built-in mechanism for installing and sharing configurations (i.e. agents, roles, skills, macros, tools, and MCP servers) directly from any git repository. A shareable repository is called a bundle; bundles are Coyote's equivalent of plugins in other CLI agents. Bundles make it easy to:
- Sync your Coyote setup across multiple machines.
- Share your work with teammates or the community.
- Bootstrap a new install with a curated set of assets.
- Pin to specific versions for reproducibility.
Coyote tracks every bundle it installs, so bundles have a full lifecycle:
| Action | CLI | REPL |
|---|---|---|
| Install or update | coyote --install <source-or-name> |
.install <value> |
| List installed | coyote --list-bundles |
.list bundles |
| Update from source | coyote --update-bundle <name> |
|
| Uninstall | coyote --uninstall <name> [--yes] |
.uninstall <name> |
| Reinstall built-ins | coyote --install-builtins <category> |
.install <category> |
This page covers the expected repository layout, bundle identity, provenance tracking, conflict resolution, secrets handling, and how to publish your own bundle.
To see a template repository with all recognized categories, see the coyote-bundle-template repository.
Install everything from a GitHub repo using owner/repo shorthand:
coyote --install someuser/oh-my-coyoteCoyote expands the shorthand against github.com (printing Resolved 'someuser/oh-my-coyote' to 'https://github.com/someuser/oh-my-coyote'), clones the repo to a temp directory, scans for recognized asset
categories, and installs each into the matching subdirectory of your user config. The temp clone is removed on
completion, and the install is recorded in the bundle store so you can list, update, and uninstall it later.
Full URLs work everywhere shorthand does:
coyote --install https://github.com/someuser/oh-my-coyoteor from inside the Coyote REPL:
.install someuser/oh-my-coyote
⚠️ Heads up: Sandbox implications. If you use Sandbox mode and the bundle includes anysbx-mixin.yamlfiles, installing it grants those mixins network access and install privileges inside your sandboxes the next time youcoyote --sandbox. See Sandbox Implications at the bottom of this page before installing bundles from sources you don't fully trust.
--install accepts one value that Coyote classifies automatically:
| Value shape | Interpretation |
|---|---|
https://..., git@host:path, file://..., ./dir, /abs, ~/dir
|
A remote source; cloned and installed. |
owner/repo (two or more segments) |
Shorthand; expanded to https://<git-host>/owner/repo and installed. |
| An installed bundle's name | The bundle is updated from its recorded source. |
An asset category (agents, ...) |
An error pointing you at --install-builtins <category> instead. |
| Anything else | An error listing your installed bundles. |
Bare names never trigger a clone, so a typo'd bundle name can't silently install something from the network.
The shorthand defaults to github.com. To install from a different host, pass --git-host:
coyote --install someuser/oh-my-coyote # github.com
coyote --install --git-host git.somedomain.com someuser/oh-my-coyote # self-hosted
coyote --install --git-host gitlab.com group/subgroup/repo # nested groups workDetails:
- Shorthand takes two or more path segments, so GitLab-style nested subgroups expand correctly.
- Expansion always uses
https://. For private repos you authenticate over SSH, use the fullgit@host:owner/repo.gitURL instead (see Git authentication). -
--git-hostrequires--install, only accepts shorthand values (passing it with a full URL is an error), and forces the value to be treated as a source even if it happens to match an installed bundle's name. -
Ref pinning works on shorthand:
coyote --install someuser/repo#v1.2.0. - The REPL form accepts the same flag:
.install someuser/repo --git-host git.somedomain.com.
Pin the install to a specific tag, branch, or commit by appending #<ref> to the URL or shorthand.
coyote --install someuser/repo#v1.2.3
coyote --install https://github.com/<owner>/<repo>#main
coyote --install https://github.com/<owner>/<repo>#abc1234How Coyote resolves the ref:
-
Branch or tag names are passed as
--branch <ref>to a shallowgit clone --depth 1. -
Commit SHAs (4-40 hex characters) trigger a full clone followed by
git checkout <ref>.
Validation: refs must match [A-Za-z0-9._/+-], must not start with -, and must not contain ... These rules prevent
the ref from being interpreted as a CLI flag or escaping the repo via path traversal.
The pin is recorded with the bundle, and --update-bundle <name>#<new-ref> moves it later.
Restrict an install to a single asset category with --filter:
| Filter | Installs |
|---|---|
| (omitted) |
agents/, roles/, skills/, macros/, functions/tools/, and merges mcp.json
|
agents |
agents/ only |
roles |
roles/ only |
skills |
skills/ only |
macros |
macros/ only |
functions |
functions/tools/ only (does not include mcp.json) |
mcp-config |
mcp.json only (merged) |
coyote --install <source> --filter agents
coyote --install <source> --filter mcp-configREPL form:
.install <source> --filter agents
Note that --filter functions is intentionally narrow. It installs the global tools under functions/tools/ and
not mcp.json. To install just the MCP config, use --filter mcp-config. To install both, use no filter.
Repeated filtered installs of the same bundle merge into a single bundle record, and a later update always processes the whole remote, including categories a filtered install excluded.
Coyote recognizes these top-level directories. Anything outside them is ignored.
<repo>/
├── coyote-bundle.yaml # Optional bundle manifest (identity only)
├── agents/
│ └── <agent-name>/ # One subdirectory per agent
│ ├── config.yaml # LLM-loop agent
│ │ └── (or graph.yaml) # Graph agent
│ ├── README.md # Optional
│ ├── tools.sh # Optional agent-local tools
│ └── scripts/ # Optional graph-node scripts
├── roles/
│ └── <role-name>.md # Markdown with YAML frontmatter + prompt body
├── skills/
│ └── <skill-name>/ # One subdirectory per skill
│ └── SKILL.md # YAML frontmatter + body
├── macros/
│ └── <macro-name>.yaml # Positional/rest variables + REPL command steps
├── functions/
│ ├── tools/
│ │ └── *.sh / *.py / *.ts # Global tools (auto chmod +x on install)
│ └── mcp.json # Historical MCP config location (still supported)
└── mcp.json # MCP server config (merged with local, not overwritten)
A few things to note:
-
Missing categories are skipped silently. A repo that only contains
agents/installs only agents. This means you do not need to use--filterfor partial repos. -
.git/is excluded from the scan automatically. - Symlinks are rejected by the install walker as a defense-in-depth measure.
-
mcp.jsonmay live at the bundle root or at the historicalfunctions/mcp.json. If both exist, the root-level file wins, mirroring how Coyote resolves the user-scope config location. -
functions/bin/andfunctions/utils/are not recognized (compiled at runtime / not in scope for sharing). -
The whole
config.yaml(global Coyote config) is not shared (see What is not shared).
Every installed bundle has a name. By default it's the repository name (the last URL segment, without .git), so
https://github.com/someuser/oh-my-coyote installs as oh-my-coyote.
Bundle authors can override this by adding a coyote-bundle.yaml manifest to the repository root:
name: oh-my-coyote # required; the bundle's identity
version: "1.4.0" # optional; shown in --list-bundles
description: Opinionated roles, macros, and skills for Coyote
homepage: https://github.com/example/oh-my-coyote # optionalThe manifest is identity only. It never declares the bundle's contents; those are always discovered by scanning the repository, exactly as for a manifest-less repo.
How names are kept unambiguous:
- One source, one record. Installing the same repository again (by any URL spelling, or via shorthand) updates the existing record instead of creating a second one. If the repo's manifest name changed since the last install, the record migrates to the new name and Coyote prints a notice.
-
Cross-source collisions are owner-qualified. If you install two different repositories that both want the name
oh-my-coyote, the second is recorded as<owner>/oh-my-coyote. For derived names this is deterministic and Coyote prints a notice. When a manifest declares the colliding name, interactive installs ask for confirmation first (a fork or typo-squat is the likely cause); declining aborts before anything is written, and non-interactive installs keep the deterministic qualification. Lifecycle commands accept the qualified name. -
Category names are reserved. A bundle cannot be named
agents,roles,skills,macros,functions, ormcp-config. A repository or manifest that wants one of those names is owner-qualified at install time (e.g.x/agents), so a bundle can never shadow an asset category.
Coyote records everything an install writes in installed-bundles.yaml in your config directory. For each bundle it
tracks the source URL, ref pin, resolved commit, manifest metadata, timestamps, every file written (with its content
hash and category), and every mcp.json server entry the merge added, replaced, or renamed (with the hash of the
entry as written).
This record is what powers the rest of the lifecycle:
- Ownership. A file belongs to the bundle that wrote its current content. If a later install overwrites a file another bundle owned, ownership transfers. Files you chose to keep during conflict prompts are never claimed.
- Abort safety. Provenance is recorded as files are written, so aborting mid-install (e.g. at a conflict prompt) leaves everything already on disk tracked and uninstallable.
-
Drift detection. The recorded hashes let
--list-bundlestell you which files you've modified since install, and let update/uninstall treat your modified files more carefully than pristine ones. -
No secrets. Recorded hashes cover the content as written, and MCP entries are written with their
{{SECRET}}placeholders intact, so the store never contains secret material.
The store is plain YAML and safe to read, but hand-editing it will make Coyote's view of ownership drift from reality. If it becomes corrupt, Coyote refuses to treat it as empty (that would let a reinstall re-claim files you've since modified) and reports the parse error instead.
coyote --list-bundlesor .list bundles in the REPL. Output is a table:
| Column | Meaning |
|---|---|
name |
The bundle's identity (manifest name, repo name, or owner-qualified name). |
version |
The manifest version, or the short commit hash if the manifest doesn't set one. |
source |
The recorded source URL updates are pulled from. |
ref |
The #<ref> pin, or - if the install tracked the default branch. |
installed |
When the bundle was first installed. |
files |
Per-category counts of owned files (e.g. macros: 3, roles: 2). |
drift |
File status vs. the recorded hashes: intact, modified locally, and missing. |
Drift statuses:
- intact: the file on disk still matches the content the bundle installed.
- modified locally: you've edited the file since install (or it can't be read).
- missing: the file was deleted from disk.
coyote --update-bundle <name> # update from the recorded source
coyote --update-bundle <name>#<ref> # also move the ref pin
coyote --install <name> # same thing; installed names dispatch to updateAn update re-clones the recorded source (at the recorded pin unless you move it) and re-runs the install with provenance-aware conflict handling:
- Files the bundle owns that you haven't touched (hash still matches) are refreshed without prompting. The bundle wrote that content; bringing it up to date is not a conflict.
- Files you've modified locally, and files owned by other bundles, get the normal conflict prompts.
-
The whole remote is processed, including categories a previous
--filterexcluded. - Files the remote no longer ships are reconciled: a file already deleted from disk just drops out of the record; a file still present is kept by default (you may rely on it) and only deleted if you confirm. Kept files stay in the record so a later uninstall still offers to remove them.
--filter and --install-force do not apply to updates by name; they are flags for remote installs.
coyote --uninstall <name>
coyote --uninstall <name> --yes # skip confirmation promptsor .uninstall <name> in the REPL. The spec can be the bundle name, its source URL, or owner/repo shorthand.
What gets removed:
- Owned files whose content still matches the recorded hash are deleted, and directories left empty by the deletions are pruned.
-
Owned files you've modified locally are kept unless you explicitly confirm their deletion at a prompt
(
--yesdoes not delete them; it only skips confirmations). -
mcp.jsonentries the bundle added are removed if unmodified; modified entries prompt like files. Entries that existed before the bundle replaced them, and keys the bundle never owned, are never touched. -
The bundle's record is dropped once nothing it owned remains. If items were kept or a deletion failed, the
record keeps them and re-running
--uninstalloffers them again.
Not removed: vault secrets the bundle's servers referenced (the uninstall summary lists them with a note that they
were installed by this bundle but not removed, since secrets may be shared with other servers), enabled_* config
lists that mention its assets, and compiled tool binaries under functions/bin/ (those linger until the next
--build-tools prune; Coyote prints a note when this applies).
Uninstalling is confirmation-gated: in a terminal you're shown what the bundle owns and asked to proceed;
non-interactive runs (CI, piped) require --yes.
--uninstall someuser/oh-my-coyote resolves in strict priority order: exact bundle name, then exact source URL, then
a match against the recorded sources' trailing path segments. If several bundles were installed from different hosts
under the same owner/repo path, Coyote shows an interactive selector listing each candidate as name (source) so
you choose exactly which one to remove. There is no auto-picking: non-interactive runs bail and list the candidates
(re-run with the exact bundle name or source URL), and --yes never selects on your behalf.
When an install file would overwrite an existing local file with different contents, you have several options.
In a terminal (TTY), Coyote prompts per conflicting file:
| Option | Effect |
|---|---|
keep |
Skip this file; keep your local copy. |
replace |
Overwrite with the remote file. |
keep-all |
Skip this file and all remaining conflicts in this install. |
replace-all |
Overwrite this file and all remaining conflicts in this install. |
abort |
Stop the install. Files already written stay; they are not rolled back (and remain tracked in the bundle's record). |
To skip prompts and replace everything, pass --install-force on the CLI or --force in the REPL form. In non-TTY
mode (CI, piped, redirected stdin), the install will abort rather than silently overwrite. To overwrite
non-interactively, you must pass --install-force.
During updates, files the bundle owns and you haven't modified skip these prompts entirely and are refreshed in place.
If a remote file's bytes match your local copy exactly, Coyote silently treats it as identical and skips it. Re-running
the same install is idempotent.
Scripts written into functions/tools/ are automatically marked executable based on extension. Bash (.sh), Python
(.py), and TypeScript (.ts) files get chmod 0o755 on Unix. This applies to both new files and replaced conflicts.
The bundle's mcp.json (at the repo root, or the historical functions/mcp.json location) is the exception to
the per-file conflict model. Instead of replacing your local file outright, Coyote merges the remote mcp.json
into your existing one.
- New server names (present in remote, absent locally) are added.
- Identical server entries (same JSON shape locally and remotely) are silently kept.
- Conflicting server entries prompt with three options:
| Option | Effect |
|---|---|
keep local |
Leave your existing entry alone. |
take remote |
Overwrite with the remote entry. |
rename remote as "<name>-remote" |
Insert the remote entry under a suffixed key. If the suffixed key is also taken, Coyote appends -2, -3, etc. |
abort merge |
Stop the merge with an error. No partial write. |
With --install-force, every conflict is resolved by taking the remote entry. In non-TTY mode without
--install-force, the merge aborts before writing.
Entries the merge adds, replaces, or renames are recorded as owned by the bundle (with the hash of the entry as written), which is how uninstall knows what it may safely remove later. Entries you kept local are not claimed.
Each added, replaced, or renamed entry is validated against the MCP server schema (e.g., stdio servers must have a
command; http/sse servers must have a url) before the merged file is written. A validation failure aborts the
install. Coyote will never write a partially-validated mcp.json.
The merged file is written to a tempfile (mcp.json.tmp) and then renamed atomically over the target. This guarantees
that a crash or interrupt mid-write will not corrupt your existing mcp.json.
MCP server entries (and other config files) can reference vault secrets with {{SECRET_NAME}} placeholders. After the
install completes, Coyote scans the resulting mcp.json for placeholders that are not yet in your vault and either:
- In a TTY: prompts you, one secret at a time, whether to add it to the vault now. On the first "Yes", Coyote initializes the vault (if needed; for the Local provider, this just means creating the password file). On "No", the secret is deferred and reported at the end.
-
In a non-TTY environment: skips prompts entirely; lists every missing secret in a final reminder block, with the
commands you can run later (
coyote --add-secret <NAME>or.vault add <NAME>).
Both modes always print a final summary distinguishing secrets added from those deferred. See Vault for the full secrets workflow.
Coyote shells out to your local git binary to clone the remote repository. This means it inherits your existing git
authentication setup with no additional configuration:
-
SSH URLs (
git@github.com:owner/repo.git) use yourssh-agent,~/.ssh/config, and SSH keys. -
HTTPS URLs with private repos use your configured
credential.helper(e.g., osxkeychain on macOS, libsecret on Linux, the GitHub CLI's helper). - Public HTTPS URLs require no auth.
Note that owner/repo shorthand always expands to an HTTPS URL. If your access to a private repo only works over
SSH, install with the full SSH URL instead.
If git is not on your PATH, you'll get a clear error message on the first install attempt.
coyote --install-builtins <category> (REPL: .install <category>) reinstalls the assets that ship inside the
Coyote binary for one category (agents, roles, skills, macros, functions, themes), overwriting any local
changes to those files. Built-in assets are not tracked in the bundle store, don't appear in --list-bundles, and
can't be uninstalled; they're part of Coyote itself.
Passing a category name to --install never reinstalls built-ins by accident; it errors and points you at
--install-builtins (and, if you also have a bundle by that name, at --update-bundle).
To share your Coyote configuration, structure a git repo like the layout above and push it to GitHub, GitLab, your self-hosted Forgejo instance, etc.
The easiest way to start is to fork the official template repo:
coyote-bundle-template. It includes a working sample of each
asset type plus a README describing the customization workflow.
-
Add a
coyote-bundle.yamlwith a stablenameand aversionyou bump on releases. This keeps your bundle's identity stable even if the repo is renamed or forked, and gives installers a meaningful version in--list-bundles. -
Pin to tagged releases so consumers can install with
#<tag>for reproducibility, and move pins deliberately with--update-bundle <name>#<tag>. -
Keep per-agent logic in
agents/<name>/. Only put global tools infunctions/tools/. -
Use
{{SECRET}}placeholders for any sensitive value inmcp.json. Never commit a real API key. -
Document expected secrets in your repo's
READMEso users know which vault entries to add. -
Avoid name collisions with Coyote's bundled assets (
agents/code-reviewer,agents/sql, etc.) unless you specifically want to replace them. Pick distinctive names for your shared assets. -
Test installs against a clean
COYOTE_CONFIG_DIRbefore publishing. You can use:COYOTE_CONFIG_DIR=$(mktemp -d) coyote --install file:///path/to/your/clone --install-force
The following are intentionally outside the bundle feature's scope:
-
config.yaml(the global Coyote config). It holds user-specific things like editor preference, secrets provider configuration, client API keys, OAuth tokens, and similar. Merging a sharedconfig.yamlwould risk breaking auth or routing traffic somewhere unexpected. Settings that genuinely benefit from sharing (like default models) already belong inside individual agents' or roles' configs. - Sessions, RAGs, agent runtime data. These accumulate as you use Coyote and aren't shareable in a meaningful way.
-
functions/bin/: Agent script binaries built at runtime, not part of the source layout. -
functions/utils/: Utility scripts intentionally not part of the share contract.
If a team really needs synced global settings, the recommended approach is dotfiles-style symlinks or a sync tool.
Those are designed for that problem and not for coyote --install.
coyote --install someuser/oh-my-coyotecoyote --install --git-host git.somedomain.com someuser/oh-my-coyotecoyote --install your-org/coyote-config#v2.4.0coyote --install https://github.com/your-org/coyote-config --filter agentscoyote --install https://github.com/your-org/coyote-config --filter mcp-config --install-forcecoyote --list-bundles
coyote --update-bundle oh-my-coyote
coyote --update-bundle coyote-config#v2.5.0 # move the pincoyote --uninstall oh-my-coyote --yes.install your-org/coyote-config
.install https://github.com/your-org/coyote-config#main --filter agents
.install someuser/repo --git-host git.somedomain.com
.list bundles
.uninstall oh-my-coyote
coyote --install git@github.com:your-org/private-coyote-config.git#v1.0.0coyote --install file:///home/you/code/my-coyote-configThe remote clone failed. Coyote passes git's stderr through verbatim. The most common causes:
-
Authentication required:
fatal: could not read username/passwordorPermission denied (publickey). Verify your git credentials work for the same URL outside Coyote by runninggit clone <url>manually. Remember thatowner/reposhorthand expands to HTTPS; use the full SSH URL for SSH-only private repos. -
Network failure:
Could not resolve hostor similar. Check connectivity. -
Invalid ref:
couldn't find remote ref <ref>. Verify the tag/branch/commit exists in the remote.
You passed a category name to --install. Use coyote --install-builtins agents to reinstall Coyote's built-in
assets for that category, or --update-bundle <name> if you meant a bundle that happens to share the name.
The value didn't match an installed bundle, and it isn't shaped like a source (git URL, owner/repo shorthand,
scp-style host:path, or an explicit local path). Check --list-bundles for the exact bundle name, or pass a full
source to install something new.
You uninstalled by owner/repo shorthand in a non-interactive run and more than one installed bundle matches that
path on different hosts. Re-run with the exact bundle name or source URL (both shown in the error and in
--list-bundles), or run interactively to get a selector.
The cloned repo has none of the recognized top-level directories. Make sure the repo's layout matches the
expected layout. A README.md and a LICENSE at the root are fine; they're just
ignored.
You're running in non-TTY mode (CI, piped, redirected stdin) and there's a conflict. Re-run with --install-force to
overwrite, or run in an interactive terminal to be prompted.
Same pattern as the above, but for the MCP merge. Use --install-force to take the remote entry for every conflict.
Add them via coyote --add-secret <NAME> (CLI) or .vault add <NAME> (REPL). See Vault for details.
Coyote shells out to the system git binary. Install git for your platform and ensure it's on your PATH.
If you use Coyote's Sandbox mode, installing a shared bundle that contains any sbx-mixin.yaml
files is a privilege escalation event. Those mixins are auto-discovered and applied silently on your next
coyote --sandbox, granting:
-
Network access to every domain listed in the mixin's
permissions.network.allow(v1:network.allowedDomains). - Install commands run with passwordless sudo inside the sandbox (the agent user has full sudo).
-
Environment variables set in
environment.variables.
This is fine for bundles you trust (your own configs, your team's shared repo). It can be dangerous for bundles from unknown sources.
-
Grep the repo for
sbx-mixin.yamlfiles and read each one, then list any siblingfiles/trees they'll drop into your sandbox:git clone --depth 1 https://github.com/<source>/<repo> /tmp/audit find /tmp/audit -name 'sbx-mixin.yaml' -exec cat {} + find /tmp/audit -type d -name files -path '*/agents/*' -o -type d -name files -path '*/functions/*'
-
Look specifically at:
-
permissions.network.allow(v1:network.allowedDomains): What domains are being added to the sandbox's allowlist? -
setup.install(v1:commands.install): What commands run with passwordless sudo? Do they pull from a recognized source (apt repo, official installer, signed release) or arbitrary URLs? -
environment.variables: Do any look like credential exfiltration vectors (*_API_URLpointing at unknown hosts,LD_PRELOAD, etc.)? -
Sibling
files/trees next to anysbx-mixin.yaml: These are auto-mounted into the sandbox via sbx's static-files convention. Look for anything landing atfiles/home/.config/*(agent config overrides),files/home/.local/bin/*(executables on$PATH), orfiles/workspace/*(files dropped into your project dir). See Sandboxes: Bundling static files alongside a mixin.
-
-
First-run with
--no-mixinsto verify the bundle's non-sandbox features work without granting any sandbox elevation:Then re-launch withoutcoyote --sandbox skeptical-test --fresh --no-mixins
--no-mixinsonce you've reviewed and accepted the mixins.
Every coyote --sandbox prints a verbose mixin log listing each mixin about to be
applied along with its install/domain counts. Skim it the first time a bundle's mixin is applied. If something looks
off, abort with Ctrl-C (only safe before sbx create starts running install commands; once installs begin, use
sbx rm <name> after to clean up).
If you publish a config repo, document any sbx-mixin.yaml files in your README.md. Explain what they add to
the sandbox and why, and if any of them ship a sibling files/ tree, spell out what lands where (e.g. "installs a
~/.gitconfig and a helper at ~/.local/bin/foo"). Users who land on your repo will trust you more if you make the
privilege grants explicit instead of letting them discover via grep.