Skip to content

Bundles

Alex Clarke edited this page Aug 28, 2026 · 2 revisions

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.


Quick start

Install everything from a GitHub repo using owner/repo shorthand:

coyote --install someuser/oh-my-coyote

Coyote 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-coyote

or from inside the Coyote REPL:

.install someuser/oh-my-coyote

⚠️ Heads up: Sandbox implications. If you use Sandbox mode and the bundle includes any sbx-mixin.yaml files, installing it grants those mixins network access and install privileges inside your sandboxes the next time you coyote --sandbox. See Sandbox Implications at the bottom of this page before installing bundles from sources you don't fully trust.


Install sources

--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.

owner/repo shorthand and --git-host

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 work

Details:

  • 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 full git@host:owner/repo.git URL instead (see Git authentication).
  • --git-host requires --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.

Ref pinning

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>#abc1234

How Coyote resolves the ref:

  • Branch or tag names are passed as --branch <ref> to a shallow git 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.

Filtering by category

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-config

REPL 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.


Expected repository layout

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 --filter for partial repos.
  • .git/ is excluded from the scan automatically.
  • Symlinks are rejected by the install walker as a defense-in-depth measure.
  • mcp.json may live at the bundle root or at the historical functions/mcp.json. If both exist, the root-level file wins, mirroring how Coyote resolves the user-scope config location.
  • functions/bin/ and functions/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).

Bundle identity and the manifest

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   # optional

The 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, or mcp-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.

Provenance and ownership

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-bundles tell 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.


Listing bundles and drift

coyote --list-bundles

or .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.

Updating a bundle

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 update

An 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 --filter excluded.
  • 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.


Uninstalling a bundle

coyote --uninstall <name>
coyote --uninstall <name> --yes        # skip confirmation prompts

or .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 (--yes does not delete them; it only skips confirmations).
  • mcp.json entries 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 --uninstall offers 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.

Ambiguous owner/repo specs

--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.


Conflict resolution

When an install file would overwrite an existing local file with different contents, you have several options.

Plain files (agents, roles, skills, macros, tools)

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.

Identical content is not a conflict

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.

File-mode (+x) handling

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.


MCP configuration merge

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.

Merge behavior

  • 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.

Validation

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.

Atomic write

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.


Secrets handling

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.


Git authentication

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 your ssh-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.


Built-ins are not bundles

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).


Publishing your own 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.

Best practices

  1. Add a coyote-bundle.yaml with a stable name and a version you 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.

  2. Pin to tagged releases so consumers can install with #<tag> for reproducibility, and move pins deliberately with --update-bundle <name>#<tag>.

  3. Keep per-agent logic in agents/<name>/. Only put global tools in functions/tools/.

  4. Use {{SECRET}} placeholders for any sensitive value in mcp.json. Never commit a real API key.

  5. Document expected secrets in your repo's README so users know which vault entries to add.

  6. 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.

  7. Test installs against a clean COYOTE_CONFIG_DIR before publishing. You can use:

    COYOTE_CONFIG_DIR=$(mktemp -d) coyote --install file:///path/to/your/clone --install-force

What is not shared

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 shared config.yaml would 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.


Examples

Install from GitHub with shorthand

coyote --install someuser/oh-my-coyote

Install from a self-hosted git server

coyote --install --git-host git.somedomain.com someuser/oh-my-coyote

Pin to a release tag

coyote --install your-org/coyote-config#v2.4.0

Install only the agents from a fork

coyote --install https://github.com/your-org/coyote-config --filter agents

Install only the MCP servers, force-replace conflicts

coyote --install https://github.com/your-org/coyote-config --filter mcp-config --install-force

See what you have installed, then update one bundle

coyote --list-bundles
coyote --update-bundle oh-my-coyote
coyote --update-bundle coyote-config#v2.5.0   # move the pin

Uninstall a bundle non-interactively

coyote --uninstall oh-my-coyote --yes

From the REPL

.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

From a private SSH URL

coyote --install git@github.com:your-org/private-coyote-config.git#v1.0.0

From a local file:// URL (testing your own template)

coyote --install file:///home/you/code/my-coyote-config

Troubleshooting

"git failed: ..."

The remote clone failed. Coyote passes git's stderr through verbatim. The most common causes:

  • Authentication required: fatal: could not read username/password or Permission denied (publickey). Verify your git credentials work for the same URL outside Coyote by running git clone <url> manually. Remember that owner/repo shorthand expands to HTTPS; use the full SSH URL for SSH-only private repos.
  • Network failure: Could not resolve host or similar. Check connectivity.
  • Invalid ref: couldn't find remote ref <ref>. Verify the tag/branch/commit exists in the remote.

"'agents' is an asset category, not a bundle"

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.

"no bundle named '<value>' is installed"

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.

"'<owner>/<repo>' matches multiple installed bundles"

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.

"No recognized assets found in <url>"

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.

"Refusing to overwrite local file <path> non-interactively"

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.

"MCP server '<name>' already exists locally. Refusing to merge non-interactively."

Same pattern as the above, but for the MCP merge. Use --install-force to take the remote entry for every conflict.

Missing secrets after install

Add them via coyote --add-secret <NAME> (CLI) or .vault add <NAME> (REPL). See Vault for details.

git binary not found

Coyote shells out to the system git binary. Install git for your platform and ensure it's on your PATH.


Sandbox Implications

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.

Before installing a bundle from a source you don't fully trust

  1. Grep the repo for sbx-mixin.yaml files and read each one, then list any sibling files/ 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/*'
  2. 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_URL pointing at unknown hosts, LD_PRELOAD, etc.)?
    • Sibling files/ trees next to any sbx-mixin.yaml: These are auto-mounted into the sandbox via sbx's static-files convention. Look for anything landing at files/home/.config/* (agent config overrides), files/home/.local/bin/* (executables on $PATH), or files/workspace/* (files dropped into your project dir). See Sandboxes: Bundling static files alongside a mixin.
  3. First-run with --no-mixins to verify the bundle's non-sandbox features work without granting any sandbox elevation:
    coyote --sandbox skeptical-test --fresh --no-mixins
    Then re-launch without --no-mixins once you've reviewed and accepted the mixins.

Runtime audit

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).

When sharing your own bundle

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.

Clone this wiki locally