# What is Kasetto? Kasetto keeps AI agent skills, MCP servers, slash-commands, and instructions in one version-controlled YAML file and syncs them to 23 agents. **When you need this:** You want a quick, predictable path to "working sync" and you're not sure what Kasetto will modify. **What you'll learn:** - How to create a config and run your first sync - How to preview changes and script CI runs - Where to go next for the exact sync/merge guarantees Once you've [installed Kasetto](/docs/installation), run `kst` to make sure it's available: ```bash kst A declarative AI agent environment manager Usage: kst ... ``` Bare `kst` (no subcommand) prints the banner followed by `--help`, just like bare `cargo` or `uv`. Kasetto is CLI-only: every action has a dedicated subcommand. Pipe stdout or pass `--color never` to strip styling. ## Creating a Config Run `kst init` to generate a starter config: ```bash kst init ``` Use `kst init --global` to create `$XDG_CONFIG_HOME/kasetto/kasetto.yaml` (or `~/.config/kasetto/kasetto.yaml`). Or let Kasetto edit the config for you. `kst add ` appends a source (preserving your comments) and syncs it in, and `kst remove ` reverses it, the cargo/uv way: ```bash kst add https://github.com/org/skill-pack # add every skill kst add https://github.com/org/skill-pack@v1.2.0 # `@` shorthand (cargo/uv-style) kst add https://github.com/org/skill-pack --skill code-reviewer # or just named ones kst add https://github.com/org/skill-pack --dry-run # preview the edit, don't write kst remove https://github.com/org/skill-pack --skill code-reviewer # reverses the add ``` Or create a `kasetto.yaml` by hand: ```yaml agent: claude-code skills: - source: https://github.com/org/skill-pack branch: main skills: - code-reviewer - name: design-system ``` Use the `agent` field to target any of the [supported agents](/docs/agents), or use the `destination` field for a custom install path. ## Syncing Skills Run `kst sync`: ```bash kst sync Syncing skills from 1 source... ✓ code-reviewer (installed) ✓ design-system (installed) Synced 2 skills in 1.2s ``` Kasetto pulls the skills and installs them into the right agent directory. Next time you run `sync`, only what changed gets updated. [How Sync Works](/docs/how-sync-works) has the exact contract for what gets copied and what gets removed. ## Syncing from a Remote Config Pass a shared team config as a URL: ```bash kst sync --config https://example.com/team-skills.yaml ``` For private configs hosted on git providers, set the right env var token first (see [Authentication](/docs/authentication#remote-configs)). You can also inherit from another config with `extends:`. A child config can pull in everything from a team base and override or extend it. See [Extending Another Config](/docs/configuration#extending-another-config). ## Previewing Changes Use `--dry-run` to see what a sync would do before it writes anything: ```bash kst sync --dry-run Would install: code-reviewer, design-system Would remove: old-skill ``` See [CI & automation](/docs/ci) for recommended CI patterns. ## MCP Servers Kasetto can also manage MCP server configs. Add an `mcps` section to your config: ```yaml agent: claude-code skills: - source: https://github.com/org/skill-pack skills: "*" mcps: - source: https://github.com/org/mcp-pack mcps: "*" ``` Kasetto merges them into each agent's native settings file during sync. Nothing extra to do. If you want the exact merge rules and conflict behavior, see [How Sync Works](/docs/how-sync-works#mcp-servers-discovery-and-additive-merge). ## Exploring What's Installed Print a table of installed assets: ```bash kst list # all kinds together kst list --type skills # filter to one kind kst list --json # for scripting ``` With no `--project` or `--global`, **global and project** installs are shown together (each row labeled by scope). To check your local setup: ```bash kst doctor ``` Doctor shows your version, lock file location, install paths, last sync time, and any skills that failed. ## Using JSON Output `sync`, `list`, `doctor`, `clean`, and `self update` support `--json` for scripting or CI: ```bash kst sync --json kst list --json kst doctor --json kst clean --json ``` ## Next Steps See the [configuration reference](/docs/configuration) for the full config schema, browse the [commands reference](/docs/commands), or grab a ready-to-use pattern from the [cookbook](/docs/cookbook). --- # Kasetto vs the Alternatives How Kasetto compares to Vercel Skills, Claude plugin marketplaces, dotfiles and symlinks, and copying files by hand. **When you need this:** You are already managing AI agent config somehow and want to know whether Kasetto solves a problem you actually have. This page describes these tools as of August 2026. The space moves quickly - check each project's own documentation before making a decision. ## The Short Version Most tools in this space are **installers**: you run a command, a skill lands on disk, and the record of what you did is the disk itself. Kasetto is a **package manager**: the config declares what should exist, a lock file pins exactly what you got, and sync converges the disk to match. If you work in one repo, on one machine, with one agent, an installer is enough and Kasetto is overhead. The difference starts to matter at the second machine, the second agent, or the second teammate. ## Compared to Vercel Skills [Vercel Skills](https://github.com/vercel-labs/skills) installs skills from a curated catalog. It is a good way to discover and try something quickly. The differences that matter: - **Source of truth.** With a catalog installer, the answer to "what is installed here" is whatever is on disk. Kasetto's answer is `kasetto.yaml` plus `kasetto.lock`, both committable - **Where skills come from.** Kasetto takes any Git URL or local path, including private and self-hosted GitHub, GitLab, Bitbucket, Codeberg, and Gitea. There is no catalog to be accepted into - **Asset kinds.** Kasetto syncs skills, MCP servers, slash-commands, and instruction files from the same source, not skills alone ## Compared to Claude Plugin Marketplaces Plugin marketplaces give Claude runtime integrations, installed and managed through the agent itself. They are the right tool when you want a capability inside one agent and you want that agent to own its lifecycle. Kasetto sits a layer below and is agent-agnostic. It writes files into the locations agents already read, so the same source lands in Claude Code, Cursor, Codex, and 20 others from one `kst sync`. The two compose - a marketplace plugin and a Kasetto-managed skill can coexist, because Kasetto never touches entries it did not install. ## Compared to Dotfiles and Symlinks A dotfiles repo with symlinks is the honest DIY baseline, and for a single machine it works. Where it runs out: - **Merging.** A symlink replaces a file. It cannot merge shared rules into a `CLAUDE.md` that also holds repo-specific content. Kasetto uses managed comment blocks so both survive in the same file - **Per-agent formats.** Symlinking the same file into `.cursor/rules/` does not give Cursor the MDC frontmatter it expects. Kasetto transforms each asset into the target's native format - **Versioning.** Symlinks track your working copy, whatever state it is in. A lock file pins a commit, so a teammate's clone renders exactly what yours does - **Windows.** Symlinks need elevated permissions or developer mode; a static binary that copies files does not ## Compared to Copying Files by Hand This is what most people actually do, and it works right up until the copies diverge. The failure is silent: one repo's conventions quietly go stale and the agent keeps confidently applying the old ones. ## Feature Comparison | | Kasetto | Catalog installers | Plugin marketplaces | Dotfiles + symlinks | | --- | --- | --- | --- | --- | | Declarative config file | yes | no | no | partial | | Lock file with pinned commits | yes | no | no | no | | Multiple agents from one source | yes | no | no | manual | | Skills, MCPs, commands, instructions | yes | skills | varies | manual | | Arbitrary and private Git sources | yes | catalog | marketplace | yes | | Merges into existing files | yes | n/a | n/a | no | | Secret injection at sync time | yes | no | no | no | | Works offline from the lock | yes | no | no | yes | ## When Not to Use Kasetto Being direct about this is more useful than a feature list: - **One repo, one agent, no team.** Copy the file. You do not need a lock file - **You want runtime behavior inside one agent.** That is what plugins are for. Kasetto only moves files into place; it never executes what it installs - **Your skills are not in Git.** Kasetto reads Git repositories and local directories. If your assets live somewhere else, there is nothing for it to resolve ## Next Steps - [What is Kasetto?](/docs) - the five-minute tour - [Cookbook](/docs/cookbook) - team, monorepo, and pinned-rollout setups - [How sync works](/docs/how-sync-works) - the lock contract and merge rules --- # Installing Kasetto Install the Kasetto CLI on macOS, Linux, or Windows via the standalone installer, Homebrew, Scoop, or cargo. ## Installation Methods Pick whichever method works for you: standalone installer, package manager, or straight from source. ### Standalone Installer The quickest way to get started. Downloads and installs the binary in one command: Use `curl` to download the script and execute it with `sh`: ```bash curl -fsSL kasetto.dev/install | sh ``` Use `irm` to download the script and execute it with `iex`: ```pwsh-session PS> powershell -ExecutionPolicy Bypass -c "irm kasetto.dev/install.ps1 | iex" ``` Changing the [execution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies) allows running a script from the internet. The installation script may be inspected before use: ```bash curl -fsSL https://raw.githubusercontent.com/pivoshenko/kasetto/main/scripts/install.sh | less ``` ```pwsh-session PS> powershell -c "irm kasetto.dev/install.ps1 | more" ``` Alternatively, binaries can be downloaded directly from [GitHub Releases](#github-releases). The binary lands in `~/.local/bin` by default. Override it with environment variables: | Variable | Description | Default | | --------------------- | ---------------------- | ------------------------------------------------------------ | | `KASETTO_VERSION` | Version tag to install | Latest release | | `KASETTO_INSTALL_DIR` | Installation directory | `~/.local/bin` (Unix) / `%USERPROFILE%\.local\bin` (Windows) | ### Homebrew Available via a Homebrew tap: ```bash brew install pivoshenko/tap/kasetto ``` ### Scoop Available via a Scoop bucket on Windows: ```bash scoop bucket add kasetto https://github.com/pivoshenko/scoop-bucket scoop install kasetto ``` ### Cargo Available on [crates.io](https://crates.io): ```bash cargo install kasetto ``` This builds from source, so you'll need a compatible Rust toolchain. ### GitHub Releases Binaries for every supported platform are attached to each [GitHub Release](https://github.com/pivoshenko/kasetto/releases). ### From Source Clone and install with Cargo: ```bash git clone https://github.com/pivoshenko/kasetto && cd kasetto cargo install --path . ``` ## Upgrading If you used the standalone installer, updating is a one-liner: ```bash kst self update ``` For Homebrew or Cargo installs, use the package manager's own upgrade command. For example, with Cargo: ```bash cargo install kasetto ``` ## Shell Autocompletion You can run `echo $SHELL` to help determine your shell. To get tab completions for `kst`, add one of these to your shell config: ```bash echo 'eval "$(kst completions bash)"' >> ~/.bashrc ``` ```bash echo 'eval "$(kst completions zsh)"' >> ~/.zshrc ``` ```bash echo 'kst completions fish | source' > ~/.config/fish/completions/kst.fish ``` ```powershell if (!(Test-Path -Path $PROFILE)) { New-Item -ItemType File -Path $PROFILE -Force } Add-Content -Path $PROFILE -Value '(& kst completions powershell) | Out-String | Invoke-Expression' ``` Then restart your shell or source the config file. ## Next Steps Check out the [quick start](/docs/index), or jump straight to the [configuration reference](/docs/configuration) if you already know what you want. --- # Configuring kasetto.yaml Declare skills, commands, MCP servers, and instructions in one kasetto.yaml - sources, agents, scope, and composing configs with extends. When `--config` is omitted, Kasetto looks for config in this order: 1. `$KASETTO_CONFIG` env var 2. `./kasetto.yaml` 3. `source:` key in `$XDG_CONFIG_HOME/kasetto/config.yaml` 4. `$XDG_CONFIG_HOME/kasetto/kasetto.yaml` (or `~/.config/kasetto/kasetto.yaml`) Point it at a specific file or URL with `--config`, or run `kst init` for local `./kasetto.yaml` (`kst init --global` writes the global config file). To persist a remote URL as your default, add a `source:` key to `~/.config/kasetto/config.yaml` (see [Persisting a Default Remote Config](#persisting-a-default-remote-config)). Two similarly named files live in `~/.config/kasetto/`, and they do different jobs: - **`kasetto.yaml`** is a config. It holds your agents, skills, MCPs, commands, and instructions. `kst init --global` writes this one - **`config.yaml`** is a preferences file. Its only job is a `source:` key naming the config to use, and it never contains skills itself Because `./kasetto.yaml` is checked first, a project that has its own config always wins over a persisted `source:`. ## Example {/* kasetto-config:start */} ```yaml # Option A: preset destination by agent (see README for supported agent values) agent: - codex - claude-code # Option B: manual destination (takes precedence if both are set) # destination: ./.agents/skills skills: # "*" syncs every skill in the source. Each is a directory with a SKILL.md, # discovered in the source root or its skills/ subdirectory - source: https://github.com/vercel-labs/next-skills # ref: v1.0.0 # pin to a tag or commit; omit to track the default branch skills: "*" # or list skills by name - source: https://github.com/anthropics/skills skills: - doc-coauthoring - frontend-design - pptx # sub-dir: resolve the named skills under this path, e.g. skills/productivity/grill-me/ - source: https://github.com/mattpocock/skills sub-dir: skills/productivity skills: - grill-me - caveman # path: a skill in a non-standard location → //, here skills/engineering/improve-codebase-architecture/ - source: https://github.com/mattpocock/skills skills: - name: improve-codebase-architecture path: skills/engineering commands: # names resolve to commands/.md in the source (nested dirs namespace, e.g. git:commit) - source: https://github.com/gsd-build/get-shit-done commands: - gsd:explore - gsd:fast instructions: # instructions wire CLAUDE.md / .cursor/rules / AGENTS.md etc. from instructions/.{md,mdc} # "*" syncs every instruction; aggregate files (CLAUDE.md, AGENTS.md) get managed blocks - source: https://github.com/pivoshenko/pivoshenko.ai instructions: - docs-autoupdate - multi-agent-dispatch mcps: # names resolve to mcps/.json in the source - source: https://github.com/pivoshenko/pivoshenko.ai branch: main # track a specific branch (use ref: to pin a tag or commit) mcps: - github - vercel - kaggle ``` {/* kasetto-config:end */} ## Reference ### Top-Level Fields | Key | Required | Description | | ------------- | -------- | ------------------------------------------------------------------- | | `agent` | no | One or more [supported agent presets](/docs/agents) - string or list | | `destination` | no | Explicit install path - overrides `agent` if both are set | | `scope` | no | `"global"` (default) or `"project"` - where to install | | `skills` | **yes** | List of skill sources | | `mcps` | no | List of MCP sources or inline definitions | | `commands` | no | List of slash-command sources | | `instructions` | no | List of instruction sources (CLAUDE.md / .cursor/rules / AGENTS.md ...) | | `secrets` | no | Secret-injection policy for `${kst_...}` placeholders (see [Secret Injection](/docs/secrets)) | | `extends` | no | Path or URL of a parent config to inherit from (string or list) | ### Skill Source Fields | Key | Required | Description | | --------- | -------- | ---------------------------------------------------------------------------------------------- | | `source` | **yes** | Git host URL or local path (GitHub, GitLab, Bitbucket, Codeberg/Gitea) | | `branch` | no | Branch for remote sources (default: `main`, falls back to `master`) | | `ref` | no | Git tag, commit SHA, or ref - takes priority over `branch` | | `sub-dir` | no | Relative subdirectory within the source used as the discovery root (`sub_dir` alias supported) | | `skills` | **yes** | `"*"` for all, or a list of names / `{ name, path }` objects | ### Skill Entry Fields Each entry in the `skills` list can be a string (the skill name) or an object: | Key | Required | Description | | ------ | -------- | --------------------------------------------------------------------------------------------- | | `name` | **yes** | Name of the skill directory to install | | `path` | no | Parent directory containing `/SKILL.md`, resolved relative to the source root (or `sub-dir` if set). Absolute paths are honored as-is. | | Form | Resolves to | | ----------------------------------- | ------------------------------------------ | | `- code-reviewer` | discovered (root or `skills/`) | | `- { name: x }` | discovered (root or `skills/`) | | `- { name: x, path: dir }` | `dir/x/SKILL.md` | | `- { name: x, path: nested/dir }` | `nested/dir/x/SKILL.md` | ### MCP Source Fields | Key | Required | Description | | -------- | -------- | -------------------------------------------------------------------- | | `source` | **yes** | Git host URL or local path containing MCP server config | | `branch` | no | Branch for remote sources (default: `main`, falls back to `master`) | | `ref` | no | Git tag, commit SHA, or ref - takes priority over `branch` | | `mcps` | **yes** | `"*"` to discover all, or a list of names / `{ name, path }` objects | When `mcps: "*"`, Kasetto auto-discovers MCP config files in this order: 1. `.mcp.json` at the source root 2. `mcp.json` at the source root 3. Any `.json` file inside the `mcps/` subdirectory ### MCP Entry Fields Each entry in the `mcps` list can be a plain string (name) or an object, the same as skill entries: | Form | Resolves to | | --------------------------------- | ----------------------- | | `- github` | `mcps/github.json` | | `- github.json` | `mcps/github.json` | | `- { name: x }` | `mcps/x.json` | | `- { name: x, path: dir }` | `dir/x.json` | | `- { name: x, path: nested/dir }` | `nested/dir/x.json` | Paths are resolved relative to the source root; absolute paths are honored as-is. `.json` is appended automatically when the name has no extension. MCP config files must contain a `mcpServers` object with server definitions. Servers are merged into each selected agent's native settings file when that agent supports MCP (e.g., `.claude.json` for Claude Code, `.cursor/mcp.json` for Cursor). Pi intentionally has no native MCP support, so the `pi` preset skips MCP sources. See [how sync works](/docs/how-sync-works) for merge behavior details. A pack can reference secrets with `${kst_}` placeholders (tokens, passwords, API keys) instead of committing them. They're resolved at sync time from the environment or a credentials file. See [Secret Injection](/docs/secrets). ### Inline MCP Definitions For machine-specific servers, omit `source` and provide `name` and `servers` directly. `servers` has the same shape as a pack file's `mcpServers` object: ```yaml mcps: - name: laptop-extras servers: gitea: type: http url: https://gitea.internal.example/mcp headers: Authorization: Bearer ${kst:pass:tokens/gitea-mcp} ``` Inline and source-based entries can coexist; each entry uses exactly one shape, and a config with duplicate inline names fails to load. With `extends`, an inline entry replaces a parent entry with the same `name`. Inline definitions need no fetch, so `kst lock` pins them fully. Under `sync --locked`, an inline entry's hash must match the lock (a changed definition is drift), and missing agent settings are restored offline. Removing or renaming an inline entry prunes its servers. Secrets resolve only into agent settings, so use `kst sync --update laptop-extras` to rotate a secret while the definition is unchanged. Edit inline entries directly in YAML; `kst add` and `kst remove` manage source-based entries. ### Instruction Source Fields Instructions wire each agent's instruction file (`CLAUDE.md`, `.cursor/rules/*.mdc`, `AGENTS.md`, `.github/copilot-instructions.md`, and so on) from a single source. An instruction is Markdown with optional YAML frontmatter, discovered in the source's `instructions/` directory. | Key | Required | Description | | --------- | -------- | ---------------------------------------------------------------------------------------------- | | `source` | **yes** | Git host URL or local path containing a `instructions/` directory | | `branch` | no | Branch for remote sources (default: `main`, falls back to `master`) | | `ref` | no | Git tag, commit SHA, or ref - takes priority over `branch` | | `sub-dir` | no | Relative subdirectory within the source used as the discovery root (`sub_dir` alias supported) | | `instructions` | **yes** | `"*"` to discover all, or a list of names / `{ name, path }` objects | When `instructions: "*"`, Kasetto walks `instructions/**/*.{md,mdc}` (nested directories namespace with `:`, e.g. `house:security`). ### Instruction Entry Fields Each entry in the `instructions` list can be a plain string (name) or an object, the same as command entries: | Form | Resolves to | | --------------------------------- | --------------------------------- | | `- style` | `instructions/style.{md,mdc}` | | `- house:security` | `instructions/house/security.{md,mdc}` | | `- { name: x, path: dir }` | `dir/x.{md,mdc}` | ### How Instructions Land per Agent Each instruction is transformed into the target agent's native format. Two destination shapes exist: - **Aggregate file**: many instructions merge into one shared file (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.github/copilot-instructions.md`, ...). Each instruction is wrapped in a managed `` comment block, so your own hand-written content and other instructions in the same file are preserved. `remove`/`clean` strip only kasetto's blocks and never delete the file - **Per-instruction directory**: one file per instruction. Cursor (`.cursor/rules/.mdc`) gets reconstructed MDC frontmatter (`description`, `globs`, `alwaysApply`); other directory agents (`.windsurf/rules`, `.clinerules`, `.continue/rules`, ...) get the Markdown body only `globs`/`alwaysApply` are only meaningful for Cursor; they are dropped for agents that don't scope instructions. See [supported agents](/docs/agents) for the per-agent destinations. ## Extending Another Config Use `extends` to inherit from a parent config. Local relative paths resolve against the extending file's directory; HTTPS URLs are fetched with the same auth env vars as `--config`. ```yaml # child.yaml extends: ./team-base.yaml scope: project skills: - source: https://github.com/example/extra-pack skills: "*" ``` `extends` accepts a single string or a list. With a list, parents merge left-to-right; the child overrides them all. ```yaml extends: - ./org-base.yaml - https://example.com/team-overlay.yaml ``` ### Merge Rules | Field | Rule | | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `destination` | Replace - child wins | | `scope` | Replace - child wins | | `agent` | Replace - child wins | | `skills` | Merge by `(source, ref-or-branch, sub-dir)` identity. Same identity replaces; new entries append. | | `mcps` | Same as `skills` (`sub-dir` is always empty for MCP sources, so identity is `(source, ref-or-branch)`). | | `commands` | Same as `skills`. | | `instructions` | Same as `skills`. | Identity-based merging lets a child *narrow* a parent's `skills: "*"` to a specific list, or pin a different `ref`, while still adding new sources. Cycles are detected and rejected. Maximum chain depth is 8. ## Remote Configs Kasetto can fetch configs from any HTTPS URL: ```bash kst sync --config https://example.com/team-skills.yaml ``` Great for sharing a single config across a team without checking it into every repository. ### Real-World Example [pivoshenko/pivoshenko.ai](https://github.com/pivoshenko/pivoshenko.ai) is a public config that pulls skills from several community packs for Claude Code and OpenCode: ```bash kst sync --config https://github.com/pivoshenko/pivoshenko.ai/blob/main/kasetto.yaml ``` Kasetto recognises browser URLs from GitHub, GitLab, and Gitea / Codeberg / Forgejo, and auto-rewrites them to the matching raw-content endpoint. You can paste any of these directly: - `https://github.com/owner/repo/blob/main/kasetto.yaml` - `https://gitlab.com/group/repo/-/blob/main/kasetto.yaml` - `https://codeberg.org/owner/repo/src/branch/main/kasetto.yaml` Kasetto prints a short `note: rewrote browser URL to raw content: ...` line so you can see what was fetched. Authentication is resolved against the rewritten host, so the same tokens that work for raw URLs apply here too. If the URL points to a private repository, Kasetto uses the same token-based authentication as skill sources. See [authentication](/docs/authentication) for the full list of supported environment variables. ### Persisting a Default Remote Config To stop passing `--config` on every run, put the URL in the `source:` key of `~/.config/kasetto/config.yaml`: ```yaml # ~/.config/kasetto/config.yaml source: https://github.com/pivoshenko/pivoshenko.ai/blob/main/kasetto.yaml ``` Bare `kst sync` now resolves that URL on its own, on every machine where you drop the same two-line file. Browser URLs are rewritten here exactly as they are for `--config`, and private repositories authenticate the same way. Three things still take precedence, in this order: `$KASETTO_CONFIG`, an explicit `--config`, and a `./kasetto.yaml` in the current directory. That last one matters most in practice. Inside a repository that ships its own config, the persisted `source:` is ignored, so a global default and a project config coexist without either fighting the other. The value does not have to be a URL. An absolute local path works too, which is useful for keeping a config outside the repositories that consume it. ## Multiple Agents The `agent` field accepts a single value or a list. With a list, Kasetto installs each asset into every selected agent that supports that asset type: ```yaml agent: - claude-code - cursor - codex skills: - source: https://github.com/org/skill-pack skills: "*" ``` Handy when you juggle multiple agents and want them all to share the same skill set. ## Agent vs Destination If you set both, `destination` wins. Use `agent` for convenience with [supported presets](/docs/agents), or `destination` when you need full control over the install path. Use `destination` when targeting an agent that isn't in the supported list. ## Scope: Global vs Project By default, skills are installed globally into the agent's home-directory path. Add `scope: project` to your config, or pass `--project` on the command line, to install into the current project directory instead. The `--project` / `--global` flags always override whatever `scope` is set in the config file. ## Environment Variables These environment variables affect Kasetto's behavior: | Variable | Effect | | ----------------- | ----------------------------------------------------------------------------------------------- | | `NO_COLOR` | Disables colored output. Set to any value. | | `CLICOLOR_FORCE` | Forces color even when stdout is not a TTY. Set automatically by `--color always`. | | `KASETTO_CONFIG` | Overrides config discovery to use the exact path or URL (see [Configuration](#configuration)). | | `KASETTO_NO_CACHE`| Disables the on-disk source cache, forcing a fresh download + extract on every fetch. Set to any value. | | `PI_CODING_AGENT_DIR` | Overrides Pi's global resource root (default `~/.pi/agent`), matching Pi's native environment variable. | --- # CLI Command Reference Every Kasetto command: kst add, remove, lock, sync, list, doctor, init, clean, and self-update, with their flags and exit codes. ## `kst init` Generates a starter config file: local `./kasetto.yaml` by default, or global config with `--global`. ```bash kst init [OPTIONS] ``` ### Options | Flag | Description | | ---------- | --------------------------------------------------------------------------------- | | `--global` | Write `$XDG_CONFIG_HOME/kasetto/kasetto.yaml` (or `~/.config/kasetto/kasetto.yaml`) | | `--force` | Overwrite an existing config file without prompting | ## `kst add` Appends a source to your local `kasetto.yaml` and syncs it in, the cargo/uv-style way to grow a config without hand-editing YAML. Comments and formatting in the file are preserved; only the targeted lists gain a new entry. ```bash kst add [OPTIONS] ``` Kind-tagged flags `--skill`, `--mcp`, `--command`, and `--instruction` (each repeatable) name the entries to add. Because skills, MCPs, commands, and instructions are separate lists, a single `add` can write to several of them at once, which is handy for a repo that ships more than one kind. A lone `*` value (`--skill "*"`) is a wildcard. With **no** kind flags, the source is added as `skills: "*"`. ```bash kst add https://github.com/anthropics/skills # every skill in the pack kst add https://github.com/anthropics/skills@v1.2.0 # `@` shorthand (cargo/uv-style) kst add https://github.com/anthropics/skills --skill pptx --skill pdf kst add https://github.com/example/repo --skill find --mcp github --command review kst add https://github.com/example/pack --branch develop --no-sync kst add https://github.com/example/pack --dry-run # preview the edit, don't write ``` **Deep browse URLs.** Paste the URL you're looking at on GitHub/Gitea/GitLab, and `add` decomposes a `blob`/`tree` URL into the repo, ref, and sub-directory (and, for a `SKILL.md` link, the skill name): ```bash kst add https://github.com/mattpocock/skills/blob/main/skills/personal/edit-article/SKILL.md ``` writes: ```yaml skills: - source: https://github.com/mattpocock/skills branch: main sub-dir: skills/personal skills: - edit-article ``` A 40-char hex ref is pinned as `ref:`; any other ref (e.g. `main`) becomes `branch:` so `--update` keeps tracking it. Explicit `--ref`/`--branch`/`--sub-dir` override the derived pieces. For example, add a `tree/` URL for the sub-dir and name the skills yourself with `--skill`. ### Options | Flag | Description | | --------------------- | --------------------------------------------------------------------- | | `--skill ` | Skill to add (repeatable; `*` for all). Default kind when none given | | `--mcp ` | MCP server to add (repeatable; `*` for all) | | `--command ` | Command to add (repeatable; `*` for all) | | `--instruction `| Instruction to add (repeatable; `*` for all) | | `--ref ` | Pin to a git tag, commit SHA, or ref (conflicts with `--branch`) | | `--branch ` | Track a specific branch (conflicts with `--ref`) | | `--sub-dir ` | Subdirectory inside the source to use as the root (skills/commands/instructions) | | `--config ` | Config file to edit (default: `./kasetto.yaml`); must be a local file | | `--no-verify` | Skip the upfront fetch that validates the source resolves | | `--no-sync` | Edit the config without installing | | `--dry-run` | Preview the edit without writing the config or syncing | | `--locked`, `--frozen` | During the follow-up sync, never fetch; honor the lock | | `--json` | Print the edit summary as JSON | | `-q`, `--quiet` | Suppress non-error output (repeat for stricter silence) | | `--color ` | `auto` (default), `always`, or `never` | | `--project` | Sync into the current project | | `--global` | Sync globally (default) | Before writing, `add` fetches the source once to confirm it resolves (and, for a named skill list, that the names exist). Skip that with `--no-verify`. Adding a source whose `(source, ref/branch, sub-dir)` identity already exists in a list is rejected; edit it directly or `kst remove` it first. MCP entries never carry `sub-dir` (the schema has none there). Remote configs (HTTPS) can't be edited in place. The `@` shorthand (cargo/uv-style) splits the trailing `@` off the positional and is equivalent to `--ref `. Passing both the shorthand and `--ref`/`--branch` is rejected. SSH (`git@host:org/repo`) and URLs with userinfo (`https://user@host/repo`) round-trip unchanged. ## `kst remove` Deletes entries from your local `kasetto.yaml`, then syncs so the now-unconfigured skills, MCPs, commands, and instructions are pruned from disk and the lock. Aliased as `kst rm`. ```bash kst remove [OPTIONS] ``` `remove` mirrors `add`. The kind-tagged flags `--skill`, `--mcp`, `--command`, and `--instruction` (each repeatable) **subtract** named entries from a list; when the last name goes, the whole entry is dropped. A lone `*` value drops that kind's entry outright. With **no** kind flags, the source is removed from every list it appears in. ```bash kst remove https://github.com/anthropics/skills # drop the whole source kst remove https://github.com/anthropics/skills@v1.2.0 # disambiguate via `@` shorthand kst remove https://github.com/vercel-labs/skills --skill find-skills kst remove https://github.com/example/repo --mcp github --command review kst remove https://github.com/example/pack --mcp "*" # drop the whole mcps entry kst remove https://github.com/example/pack --dry-run # preview, don't write kst rm ./local/pack --no-sync # edit config only ``` Deep `blob`/`tree` browse URLs are accepted too. They resolve to the same repo-root source `add` would have written, so you can paste the URL you're looking at. ### Options | Flag | Description | | ------------------- | ---------------------------------------------------------------- | | `--skill ` | Skill to remove (repeatable; `*` drops the whole skills entry) | | `--mcp ` | MCP to remove (repeatable; `*` drops the whole mcps entry) | | `--command ` | Command to remove (repeatable; `*` drops the whole commands entry) | | `--instruction ` | Instruction to remove (repeatable; `*` drops the whole instructions entry) | | `--ref ` | Disambiguate by pinned ref when a URL appears more than once | | `--branch ` | Disambiguate by tracked branch | | `--sub-dir ` | Disambiguate by sub-dir (or pass a deep blob/tree URL) | | `--config ` | Config file to edit (default: `./kasetto.yaml`); must be local | | `--no-sync` | Edit the config without pruning installed assets | | `--dry-run` | Preview the edit without writing the config or pruning | | `--locked`, `--frozen` | During the follow-up sync, never fetch; honor the lock | | `--json` | Print the edit summary as JSON | | `-q`, `--quiet` | Suppress non-error output (repeat for stricter silence) | | `--color ` | `auto` (default), `always`, or `never` | | `--project` | Prune from the current project | | `--global` | Prune globally (default) | Subtracting a name from a `"*"` wildcard entry is an error (there are no named entries to drop; use `-- "*"` to remove the whole entry); so is naming a skill that isn't in the list. When several entries share a source URL, pass `--ref` or `--branch` to pick one. Entries inherited via `extends:` live in the parent config and must be removed there. ## `kst sync` Reads your config, fetches any remote skills, and brings your local install up to date. ```bash kst sync [OPTIONS] ``` ### Options | Flag | Description | | ------------------------ | ------------------------------------------------------------ | | `--config ` | Path or HTTPS URL to a YAML config (default order: `$KASETTO_CONFIG`, `./kasetto.yaml`, `source:` in `config.yaml`, `$XDG_CONFIG_HOME/kasetto/kasetto.yaml`) | | `--dry-run` | Preview what would change without writing anything | | `-q`, `--quiet` | Suppress non-error output (repeat for stricter silence) | | `--json` | Print the sync report as JSON | | `--color ` | `auto` (default), `always`, or `never` | | `-v`, `--verbose` | Increase detail (`-v`, `-vv`, `-vvv`); per-skill action list at `-v` | | `--project` | Install into the current project directory | | `--global` | Install globally (default) | Missing skills are reported as broken but won't stop the rest of the run: every other asset still syncs. The exit code is non-zero if anything ended up broken, so a partial sync fails CI rather than passing quietly. `--dry-run` is great in CI: verify your config without touching anything on disk. ## `kst lock` Re-resolves every source and pins it into `kasetto.lock` **without installing** to your agent directories, the equivalent of `cargo generate-lockfile` / `uv lock`. Like `sync --update`, it re-resolves moving refs (branches and the default HEAD). ```bash kst lock [OPTIONS] ``` ### Options | Flag | Description | | ------------------------ | ------------------------------------------------------ | | `--config ` | Path or HTTPS URL to a YAML config (same default order as `sync`) | | `--json` | Print the result as JSON | | `--check`, `--locked`, `--frozen` | Verify the lock matches the config without writing; exit 1 on drift | | `-P`, `--upgrade-package ...` | Only re-resolve sources providing these skills (mirrors `sync --update ...`) | | `-q`, `--quiet` | Suppress non-error output (repeat for stricter silence) | | `--color ` | `auto` (default), `always`, or `never` | | `--project` | Lock the project scope (`./kasetto.lock`) | | `--global` | Lock the global scope (default) | Skills are hashed from the materialized **source tree**. Because a skill installs as a verbatim copy, that hash equals the one a later `sync` computes at the destination, so after `kst lock` a plain `kst sync --locked` succeeds with zero fetches. Inline MCP entries need no fetch, so `lock` pins them fully. Source-based MCP and command entries can't be hashed without applying their merge/transform, so `lock` only refreshes their resolved revision pins; their content hash fills in on the next real `sync`. Any source that fails to resolve aborts the lock before writing. `kst lock` reads remote configs fine (it only writes the lock, never the config). `kst add` / `kst remove`, which rewrite the config, require a local file. ## `kst list` Prints a uv-style table of installed assets from the lock file: name, scope, and source per row. ```bash kst list [OPTIONS] ``` ### Options | Flag | Description | | ------------------- | ---------------------------------------------------------------------- | | `--type ` | `all` (default), `skills`, `mcps`, `commands`, or `instructions` | | `--json` | Output as JSON | | `-q`, `--quiet` | Suppress output (unless `--json` is set) | | `--color ` | `auto` (default), `always`, or `never` | | `--project` | Only read the project lock (`./kasetto.lock`) in the current directory | | `--global` | Only read the global lock (under XDG data) | With **no** `--project` or `--global`, Kasetto merges **both** scopes so you can see global and project installs together. Each row includes a **scope** column. The `--json` shape always includes top-level `skills`, `mcps`, `commands`, `instructions`, and `merged_scopes` keys; `--type` filters the contents of each list but never drops the key. ## `kst doctor` Prints a local health check: your version, lock file location, install paths, last sync time, and any skills that failed. It exits non-zero when it reports issues (failed assets, or locked skills missing on disk), so `kst doctor` works as a CI gate; `-q` runs the same checks and sets the same exit code without printing. It also reports **untracked entries**, assets sitting in a Kasetto-managed install path that the lock file does not track: stray skills, command files, per-directory instruction rules, orphaned instruction blocks left in a shared `CLAUDE.md`/`AGENTS.md`, and MCP servers in an agent's settings file. This is purely advisory and **read-only**. Kasetto never deletes files it does not own, and the check never changes the exit code. Because Kasetto writes no ownership marker into MCP settings files (and deliberately preserves servers you add yourself), the MCP portion is an **inventory of every server the lock does not track**, so it will also list servers you added by hand. Treat it as an FYI, not an orphan report. Command and per-directory instruction dirs are likewise shared with your own files, so anything you hand-authored there shows up too. ```bash kst doctor [OPTIONS] ``` ### Options | Flag | Description | | ---------------- | ------------------------------------------ | | `--json` | Output as JSON | | `-q`, `--quiet` | Do not print anything (unless `--json`) | | `--color ` | `auto` (default), `always`, or `never` | | `--project` | Show project-scoped diagnostics | | `--global` | Show globally-scoped diagnostics (default) | ## `kst clean` Removes everything Kasetto installed for the given scope: skills, commands, MCP configs, instructions, and lock file entries. ```bash kst clean [OPTIONS] ``` ### Options | Flag | Description | | ---------------- | --------------------------------------------------------------- | | `--dry-run` | Preview what would be removed (lists skill paths and MCP packs) | | `--json` | Print output as JSON | | `-q`, `--quiet` | Suppress non-error output | | `--color ` | `auto` (default), `always`, or `never` | | `--project` | Clean project-scoped assets | | `--global` | Clean globally-scoped assets (default) | ## `kst self` Manage Kasetto itself: update to a new version or remove it completely. ### `kst self update` Fetches the latest release from GitHub, verifies the SHA256 checksum against `checksums.txt` from the same release, and swaps out the binary in-place. ```bash kst self update [OPTIONS] ``` #### Options | Flag | Description | | -------- | -------------- | | `--json` | Output as JSON | Self-update only works when Kasetto was installed via the standalone installer. For Homebrew or Cargo installs, use their own upgrade commands. #### Update Notifications Kasetto prints a yellow `New version available: x.y.z → a.b.c` line at the end of any command when a newer release exists on GitHub. The check runs in a background thread at most once every 24 hours and stores the result under `$XDG_CACHE_HOME/kasetto/update-check.json` (default: `~/.cache/kasetto/update-check.json`). The notice is suppressed when: - output is `--json`, `--color never`, or `--quiet` - the command emits machine-readable content (`completions`, `self update`) - stdout is not a TTY (e.g. piped to a file) ### `kst self uninstall` A full teardown: removes installed skills, commands, instructions, and MCP configs, clears Kasetto's data directories, and deletes the binary. ```bash kst self uninstall [OPTIONS] ``` #### Options | Flag | Description | | ------- | -------------------------------------------------------------- | | `--yes` | Skip the confirmation prompt (required in non-interactive use) | ## `kst completions` Generates completion scripts for your shell. ```bash kst completions ``` Supported shells: `bash`, `zsh`, `fish`, `powershell`. Example for Fish: `kst completions fish > ~/.config/fish/completions/kst.fish` --- # Supported Agents All 23 agents Kasetto syncs to - Claude Code, Cursor, Codex, Pi, Copilot, Windsurf, Gemini CLI and more - and where each expects its files. Set the `agent` field in your [config](/docs/configuration) and Kasetto figures out where to put things. Each preset maps to the directory that agent expects. ## Agent Presets | Agent | Config value | Install path | | -------------- | ---------------- | ------------------------------- | | Amp | `amp` | `~/.config/agents/skills/` | | Antigravity | `antigravity` | `~/.gemini/antigravity/skills/` | | Augment | `augment` | `~/.augment/skills/` | | Claude Code | `claude-code` | `~/.claude/skills/` | | Cline | `cline` | `~/.agents/skills/` | | Codex | `codex` | `~/.agents/skills/` | | Continue | `continue` | `~/.continue/skills/` | | Cursor | `cursor` | `~/.cursor/skills/` | | Gemini CLI | `gemini-cli` | `~/.gemini/skills/` | | GitHub Copilot | `github-copilot` | `~/.copilot/skills/` | | Goose | `goose` | `~/.config/goose/skills/` | | Junie | `junie` | `~/.junie/skills/` | | Kiro CLI | `kiro-cli` | `~/.kiro/skills/` | | OpenClaw | `openclaw` | `~/.openclaw/skills/` | | OpenCode | `opencode` | `~/.config/opencode/skills/` | | OpenHands | `openhands` | `~/.openhands/skills/` | | Pi | `pi` | `~/.pi/agent/skills/` | | Replit | `replit` | `~/.config/agents/skills/` | | Roo Code | `roo` | `~/.roo/skills/` | | Trae | `trae` | `~/.trae/skills/` | | Warp | `warp` | `~/.agents/skills/` | | Windsurf | `windsurf` | `~/.codeium/windsurf/skills/` | | ZCode | `zcode` | `~/.zcode/skills/` | Pi's global paths honor `PI_CODING_AGENT_DIR` when it overrides the default `~/.pi/agent` root. ## Instruction Destinations `instructions` land in each agent's native instruction file or directory rather than the skills path above. Paths are verified against each agent's official docs. **Aggregate** files (one shared file, many instructions merged via managed comment blocks so your own edits survive) and **directories** (one file per instruction) are noted below; project paths are relative to the repo root, global paths to `$HOME`. | Agent | Project instructions path | Global instructions path | Shape | | -------------- | ------------------------------------ | ---------------------------------------------- | --------- | | Claude Code | `CLAUDE.md` | `~/.claude/CLAUDE.md` | aggregate | | Codex | `AGENTS.md` | `~/.codex/AGENTS.md` | aggregate | | OpenCode | `AGENTS.md` | `~/.config/opencode/AGENTS.md` | aggregate | | Amp | `AGENTS.md` | `~/.config/amp/AGENTS.md` | aggregate | | Gemini CLI | `GEMINI.md` | `~/.gemini/GEMINI.md` | aggregate | | Antigravity | `GEMINI.md` | `~/.gemini/GEMINI.md` | aggregate | | GitHub Copilot | `.github/copilot-instructions.md` | `~/.copilot/copilot-instructions.md` | aggregate | | Junie | `.junie/AGENTS.md` | `~/.junie/AGENTS.md` | aggregate | | Goose | `.goosehints` | `~/.config/goose/.goosehints` | aggregate | | Warp | `WARP.md` | - (UI-managed) | aggregate | | Replit | `replit.md` | - | aggregate | | OpenHands | `.openhands/microagents/repo.md` | - | aggregate | | ZCode | `AGENTS.md` | `~/.zcode/AGENTS.md` | aggregate | | Pi | `AGENTS.md` | `~/.pi/agent/AGENTS.md` | aggregate | | OpenClaw | - (no project instructions) | `~/.openclaw/workspace/AGENTS.md` | aggregate | | Cursor | `.cursor/rules/.mdc` | `~/.cursor/rules/.mdc` | directory (MDC) | | Windsurf | `.windsurf/rules/.md` | `~/.codeium/windsurf/memories/global_rules.md` | directory / global aggregate | | Cline | `.clinerules/.md` | `~/Documents/Cline/Rules/` | directory | | Continue | `.continue/rules/.md` | - | directory | | Roo Code | `.roo/rules/.md` | `~/.roo/rules/` | directory | | Augment | `.augment/rules/.md` | `~/.augment/rules/` | directory | | Kiro CLI | `.kiro/steering/.md` | `~/.kiro/steering/` | directory | | Trae | `.trae/rules/.md` | - (UI-managed) | directory | Only Cursor (`.cursor/rules/*.mdc`) gets reconstructed `description`/`globs`/`alwaysApply` frontmatter; every other agent receives the Markdown body. Many of these agents also read the cross-tool `AGENTS.md` standard as a fallback. See the [instruction source reference](/docs/configuration#instruction-source-fields) for how instructions are transformed and merged. Pi intentionally has no native MCP support, so Kasetto skips MCP sources for the `pi` preset. In a multi-agent config, those sources are still merged into every selected agent that supports MCP. ## Custom Paths Don't see your agent? Use the `destination` field to point at any path: ```yaml destination: ~/.my-custom-agent/skills ``` If both `agent` and `destination` are set, `destination` wins. See the [configuration reference](/docs/configuration#agent-vs-destination) for details. --- # Private Repository Authentication Sync skills and commands from private GitHub, GitLab, Bitbucket, Codeberg, and Gitea repositories using environment tokens. Kasetto can pull skills, commands, instructions, and MCP configs from private repositories. No login command or credentials file needed. Set an environment variable and it works. ## Supported Git Hosts | Host | Example URL | | ------------------ | --------------------------------------------------- | | GitHub | `https://github.com/org/repo` | | GitHub Enterprise | `https://ghe.example.com/org/repo` | | GitLab | `https://gitlab.com/group/project` | | GitLab self-hosted | `https://gitlab.example.com/group/subgroup/project` | | Bitbucket Cloud | `https://bitbucket.org/workspace/repo` | | Codeberg | `https://codeberg.org/owner/repo` | | Gitea | `https://gitea.com/owner/repo` | | Forgejo | `https://forgejo.org/owner/repo` | ### Host Detection Rules Kasetto identifies the git host from the URL hostname: - **GitLab** - `gitlab.com`, any subdomain of `gitlab.com` (e.g., `sub.gitlab.com`), or any hostname starting with `gitlab.` (e.g., `gitlab.example.com`) - **Bitbucket** - `bitbucket.org` or `www.bitbucket.org` - **Codeberg / Gitea / Forgejo** - `codeberg.org`, `gitea.com`, `forgejo.org` (and their `www.` variants) - **GitHub** - `github.com` and any other hostname not matching the rules above Any unrecognized hostname with an `owner/repo` path (2 segments) is treated as **GitHub Enterprise**. This means `ghe.example.com/acme/skills` works automatically with `GITHUB_TOKEN`. Unrecognized hostnames with 3+ path segments (e.g., `git.example.com/group/sub/repo`) are treated as **GitLab-style**. Self-hosted Gitea or Forgejo instances (e.g., `gitea.mycompany.com`) are **not** auto-detected. They will be treated as GitHub Enterprise. If your self-hosted instance uses the GitHub-compatible API layout this may still work, but the `GITEA_TOKEN` variable will not be applied. Use `GITHUB_TOKEN` instead, or open an issue if you need explicit support for a custom Gitea domain. ## Environment Variables ### GitHub | Variable | Description | | -------------- | ------------------------------------------- | | `GITHUB_TOKEN` | Personal access token or fine-grained token | | `GH_TOKEN` | Fallback if `GITHUB_TOKEN` is not set | Works for `github.com` and GitHub Enterprise Server alike. ```bash export GITHUB_TOKEN=ghp_... kst sync --config kasetto.yaml ``` ### GitLab | Variable | Description | | -------------- | ------------------------------------------------------ | | `GITLAB_TOKEN` | Personal or project access token | | `CI_JOB_TOKEN` | Fallback - automatically set in GitLab CI/CD pipelines | Works for `gitlab.com` and any self-hosted instance whose hostname starts with `gitlab.`. ```bash export GITLAB_TOKEN=glpat-... kst sync --config kasetto.yaml ``` ### Bitbucket Cloud Bitbucket has two options: **Method 1 - API token:** | Variable | Description | | ----------------- | --------------------- | | `BITBUCKET_EMAIL` | Account email address | | `BITBUCKET_TOKEN` | Atlassian API token | **Method 2 - App password:** | Variable | Description | | ------------------------ | ----------------------------------------------------------- | | `BITBUCKET_USERNAME` | Bitbucket username | | `BITBUCKET_APP_PASSWORD` | App password (create at Bitbucket Settings > App passwords) | Method 1 is tried first; if those variables aren't set, Method 2 is used. ### Codeberg / Gitea / Forgejo | Variable | Description | | ---------------- | --------------------------------------- | | `GITEA_TOKEN` | Personal access token | | `CODEBERG_TOKEN` | Fallback if `GITEA_TOKEN` is not set | | `FORGEJO_TOKEN` | Fallback if neither of the above is set | All three are checked in order. The first one found is used for any Gitea-family host. ## Remote Configs Authentication also applies when you fetch a config via `--config `. The token is chosen based on the URL hostname, using the same detection rules above. ```bash export GITHUB_TOKEN=ghp_... kst sync --config https://github.com/org/private-repo/blob/main/kasetto.yaml ``` If the URL points to a private resource and no matching token is set, Kasetto reports an HTTP error with a hint about which variable to set. --- # Secret Injection Keep API keys out of Git. Inject tokens into MCP server configs at sync time from your environment, 1Password, Vault, AWS, GCP, Azure, or Keychain. **When you need this:** an MCP server needs a token, password, or API key, and you don't want to commit it into the pack you share. **What you'll learn:** - How `${kst_...}` placeholders are written and resolved - Where values come from, and how to pin a specific source - What happens when a secret is missing, and how rotation works ## Placeholders Reference a secret anywhere inside an MCP server definition: a header, an `env` value, a URL. At sync time Kasetto resolves it and writes the value into the agent's settings file. There are two forms. The **chain form** `${kst_}` resolves against an ordered fallback (env, then credentials): ```json { "mcpServers": { "vercel": { "url": "https://mcp.vercel.com", "type": "http", "headers": { "Authorization": "Bearer ${kst_vercel_token}" } } } } ``` The lowercase `kst_` (or `kst:`) sentinel is the opt-in. A bare `${HOME}` or `${PATH}` is left untouched, so placeholders the agent or shell expands at launch time pass straight through. Kasetto only resolves its own sentinel. ### Nested Keys In the chain form, a double underscore (`__`) descends into a nested credentials path. A single underscore stays within one segment. | Placeholder | `credentials.yaml` path | | ------------------------ | ----------------------- | | `${kst_vercel_token}` | `kst_vercel_token` (flat) | | `${kst_vercel__token}` | `vercel.token` (nested) | | `${kst_org__ci__token}` | `org.ci.token` (nested) | Keys match case-insensitively, so `vercel.token` resolves `${kst_vercel__token}`, and the chain also tries the env var uppercased: `${kst_vercel_token}` reads `KST_VERCEL_TOKEN` from the environment. ## The Chain (env -> credentials) The chain form resolves against an ordered set of sources. The first that has the value wins: 1. **Environment variables**: the placeholder name as written, then uppercased, e.g. `${kst_vercel_token}` → `kst_vercel_token`, then `KST_VERCEL_TOKEN` 2. **`credentials.yaml`**: `$XDG_CONFIG_HOME/kasetto/credentials.yaml` (`~/.config/kasetto/credentials.yaml`), plus any extra files listed in `secrets.files` Environment wins over the file, so one shared pack works in CI (env vars) and on a laptop (`credentials.yaml`) without editing. ```yaml # ~/.config/kasetto/credentials.yaml (never commit this) kst_github_token: ghp_xxx # flat key vercel: token: xxxxxxxx # nested → ${kst_vercel__token} ``` `credentials.yaml` holds plaintext secrets. Keep it out of version control and lock it down with `chmod 600`. Kasetto warns when the file is group- or world-readable. ## Pinning a Source When you want to bypass the fallback and name exactly one source, use the **tagged form** `${kst::}`. Each tag pins exactly one provider. ### `env` - A Specific Environment Variable ```text ${kst:env:VERCEL_TOKEN} ``` Looks up `VERCEL_TOKEN` **verbatim** (no `kst_` prefix, no uppercasing). Point it at any env var you already export. ### `crd` - A credentials.yaml Path ```text ${kst:crd:vercel/token} ``` Descends the `/`-separated path in `credentials.yaml` (`vercel.token`), case-insensitively. Unlike the chain form, this never falls back to the environment. ### `op` - 1Password ```text ${kst:op://} ${kst:op:////} ``` Both forms run `op read op:////` (the [1Password CLI](https://developer.1password.com/docs/cli/)). Example: `${kst:op:Private/GitHub/token}`. ### `vault` - HashiCorp Vault ```text ${kst:vault:#} ``` Runs `vault kv get -field= ` (the [Vault CLI](https://developer.hashicorp.com/vault/docs/commands)). Example: `${kst:vault:secret/myapp#token}`. ### `kp` - KeePass ```text ${kst:kp:#} ``` Runs `keepassxc-cli show -s -a ` (the [KeePassXC CLI](https://keepassxc.org/docs/KeePassXC_UserGuide#_command_line_tool)). The attribute defaults to `Password` when `#` is omitted. Example: `${kst:kp:GitHub/PAT}` or `${kst:kp:GitHub/PAT#Token}`. Unlike `op`/`vault`, KeePass has no ambient session, so it needs the database location in config: ```yaml secrets: keepass: database: ~/secrets.kdbx # key_file: ~/secrets.keyx # optional ``` The database is unlocked with a **key-file** (`secrets.keepass.key_file`, if set) and/or a **master password** read from the `KST_KEEPASS_PASSWORD` environment variable. With neither, `kst` adds `--no-password` and closes stdin, so the CLI fails fast instead of waiting on a prompt. ### `aws` - AWS Secrets Manager ```text ${kst:aws:#} ``` Runs `aws secretsmanager get-secret-value` (the [AWS CLI](https://aws.amazon.com/cli/)) against your active profile/region. AWS secrets are often JSON, so an optional `#` extracts a top-level field. Example: `${kst:aws:prod/db}` or `${kst:aws:prod/db#password}`. ### `gcp` - Google Cloud Secret Manager ```text ${kst:gcp:#} ``` Runs `gcloud secrets versions access latest --secret=` (the [gcloud CLI](https://cloud.google.com/sdk/gcloud)) against the active project. An optional `#` extracts a top-level field when the secret is a JSON document. Example: `${kst:gcp:db-password}` or `${kst:gcp:db#password}`. ### `az` - Azure Key Vault ```text ${kst:az:/#} ``` Runs `az keyvault secret show --vault-name --name ` (the [Azure CLI](https://learn.microsoft.com/cli/azure/)). An optional `#` extracts a top-level field when the secret is a JSON document. Example: `${kst:az:my-vault/db-password}` or `${kst:az:my-vault/db#password}`. ### `pass` - pass / gopass ```text ${kst:pass:} ``` Runs `pass show ` (the [pass](https://www.passwordstore.org/) password store; gopass is compatible) and returns the first line (the password, by the store's convention). Example: `${kst:pass:work/vercel}`. ### `keychain` - macOS Keychain ```text ${kst:keychain:#} ``` Runs `security find-generic-password -s [-a ] -w` against the macOS Keychain; the `#` is optional. Example: `${kst:keychain:vercel-token}`. `op`, `vault`, `kp`, `aws`, `gcp`, `az`, `pass`, and `keychain` all shell out to the provider's own CLI at sync time and inherit whatever session and auth you already have, so no tokens live in any Kasetto file. If the CLI is missing or the lookup fails (item not found, not authenticated, locked database), the entry is a hard failure with the CLI's error, regardless of `--allow-missing-secrets`. ## Missing Secrets By default a missing required secret is a hard failure: the MCP entry is marked broken, **nothing is written** for it, and `kst sync` exits non-zero. Other assets still sync. ```bash kst sync --allow-missing-secrets ``` With `--allow-missing-secrets`, Kasetto warns instead and leaves the literal placeholder in place. The same policy can be set in config: ```yaml secrets: on_missing: warn # error (default) | warn ``` ## Rotation **Rotating a secret requires `--update`.** A plain `kst sync` will report the server `unchanged` and leave the **old** value in place. It does not re-resolve secrets on its own. When a secret-bearing server is skipped, kasetto prints a tip reminding you to run `--update`. A plain `kst sync` is stable: an MCP server already present in the agent's settings is left exactly as is, so a rotated secret in `credentials.yaml` or the environment does **not** propagate on its own. This is the same zero-churn guarantee the lock provides everywhere else. To push a rotated secret, run an update. It re-resolves and replaces the managed server block: ```bash kst sync --update # rotate everything kst sync --update vercel # rotate one server ``` `--update` overwrites only the servers Kasetto installed. Servers you added by hand, and your edits to other entries, are never touched. ## What Never Gets Stored Injection happens in memory, only on the value written to the agent destination. The plaintext secret never reaches: - **`kasetto.lock`**: the lock hashes the placeholder source file, not the resolved value - **The source cache** and stage directory: they hold the pack exactly as authored So `kasetto.lock` stays commit-safe even when the synced MCP config carries a live token. ## Config Reference The `secrets:` block is optional and carries no values, so it is safe to commit. | Key | Required | Description | | ------------ | -------- | --------------------------------------------------------------------------- | | `on_missing` | no | `error` (default) to fail, or `warn` to leave the placeholder and continue. | | `files` | no | Extra credential files (relative to the config dir, or absolute), searched after the default `credentials.yaml`. | | `keepass` | no | KeePass database for `${kst:kp:...}` refs: `database` (path) and optional `key_file`. No password (that comes from `KST_KEEPASS_PASSWORD`). | ```yaml secrets: on_missing: error files: - ./team-credentials.yaml keepass: database: ~/secrets.kdbx key_file: ~/secrets.keyx ``` Secret injection currently applies to **MCP configs**. An unknown tag (anything other than `env`, `crd`, `op`, `vault`, `kp`, `aws`, `gcp`, `az`, `pass`, or `keychain`) errors clearly rather than silently passing through. --- # Writing Agent Skills Author and publish skills that Kasetto can sync into Claude Code, Cursor, and every other supported agent. Skills are just directories with a `SKILL.md` file inside. Here's how to structure yours and what Kasetto looks for. ## Directory Layout Kasetto discovers skills from the source root and from `skills/`: ``` repo-root/ ├── SKILL.md ← discovered as repository name skill ├── my-skill/ │ └── SKILL.md ← discovered ├── skills/ │ ├── another-skill/ │ │ └── SKILL.md ← discovered │ └── third-skill/ │ └── SKILL.md ← discovered └── README.md ← ignored (no SKILL.md) ``` Kasetto picks up: - A top-level `SKILL.md` (installed using the repository name, or `sub-dir` basename) - Any root-level subdirectory containing `SKILL.md` - Any `skills//SKILL.md` subdirectory Directory names are used as skill identifiers for folder-based skills. `SKILL.md` is required. Directories without one are silently skipped. ## SKILL.md Format `SKILL.md` is a markdown file. YAML frontmatter is optional but gives you control over the metadata Kasetto records for the skill: ```markdown --- name: Code Reviewer description: Reviews pull requests for common issues and style violations. --- # Code Reviewer Detailed instructions for the AI agent go here. This is the content that gets installed into the agent's skill directory. ``` ### Frontmatter Fields | Field | Required | Description | | ------------- | -------- | -------------------------------------------------- | | `name` | no | Display name carried in `kst list --json` output | | `description` | no | Short description carried in the lock + JSON output | Both are optional. If you skip them, Kasetto parses the markdown body instead: - **Name:** First `#` heading in the document, or the directory name if no heading exists - **Description:** First non-empty, non-heading paragraph, or `"No description."` if none found ### Minimal Example No frontmatter? No problem: ```markdown # My Skill You are an expert at doing X. When the user asks you to... ``` Kasetto uses `"My Skill"` as the display name and the first paragraph as the description. ## Referencing Skills in Config Reference skills by their directory name in `kasetto.yaml`: ```yaml skills: - source: https://github.com/org/skill-pack skills: - my-skill # matches repo-root/my-skill/ or repo-root/skills/my-skill/ - another-skill ``` Want everything from a source? Use `"*"`: ```yaml skills: - source: ~/Development/my-skills skills: "*" ``` ## Custom Source Path If a skill lives somewhere non-standard within the repository, point to it with the `path` field: ```yaml skills: - source: https://github.com/acme/monorepo skills: - name: my-skill path: tools/ai-skills # looks in tools/ai-skills/my-skill/SKILL.md ``` ## Limiting Discovery to a Nested Directory If your skills live under a nested plugin folder, set `sub-dir` on the source: ```yaml skills: - source: https://github.com/acme/agents sub-dir: plugins/swift-apple-expert skills: "*" ``` Kasetto treats `plugins/swift-apple-expert` as the source root for discovery. --- # Slash Commands Share custom slash-commands and prompt templates across Claude Code, Cursor, Codex, and Gemini CLI from a single source. Kasetto can sync user-defined **slash commands** (a.k.a. custom prompt templates) into every agent that supports them. Commands are authored once in Claude-Code-style Markdown with YAML frontmatter, then transformed on the fly into each agent's native format. ## Source Format A command repository contains a `commands/` directory. Files are Markdown with YAML frontmatter: ```markdown --- description: Review the given pull request. argument-hint: allowed-tools: Bash, Read model: claude-opus-4-7 --- Review PR $ARGUMENTS and call out anything risky. ``` Subdirectories under `commands/` become `:`-separated namespaces: - `commands/commit.md` → `commit` - `commands/git/commit.md` → `git:commit` - `commands/git/work/status.md` → `git:work:status` ## Config ```yaml commands: - source: https://github.com/me/my-commands ref: v1.0 sub-dir: commands # optional - subdirectory inside the source repository commands: "*" # discover all, or list explicit entries below - source: https://github.com/acme/monorepo commands: - review-pr # namespaced name from discovery - name: deploy path: ops # → ops/deploy.md in the source ``` The `commands` field accepts the same shapes as `mcps`: - `"*"`: discover every `.md` file under `commands/` - A list of namespaced names (`git:commit`, `review-pr`) - A list of `{ name, path }` objects to load from custom subdirectories ## Per-Agent Output Formats Kasetto rewrites each command into the format expected by the destination agent: | Agent | Global directory | Project directory | Format | | --------------- | -------------------------------------- | ---------------------- | ----------------------- | | claude-code | `~/.claude/commands/` | `.claude/commands/` | Markdown + frontmatter | | cursor | - | `.cursor/commands/` | Markdown (body only) | | windsurf | `~/.codeium/windsurf/global_workflows/` | `.windsurf/workflows/` | Markdown + frontmatter | | cline | - | `.clinerules/workflows/` | Markdown (body only) | | opencode | `~/.config/opencode/commands/` | `.opencode/commands/` | Markdown + frontmatter | | continue | `~/.continue/prompts/` | `.continue/prompts/` | `.prompt` file | | github-copilot | - | `.github/prompts/` | `.prompt.md` | | amp | `~/.config/amp/commands/` | `.agents/commands/` | Markdown + frontmatter | | roo | `~/.roo/commands/` | `.roo/commands/` | Markdown + frontmatter | | augment | `~/.augment/commands/` | `.augment/commands/` | Markdown + frontmatter | | junie | - | `.junie/commands/` | Markdown + frontmatter | | openhands | - | `.openhands/microagents/` | Markdown + frontmatter | | codex | `~/.codex/prompts/` | - | Markdown + frontmatter | | gemini-cli | `~/.gemini/commands/` | `.gemini/commands/` | TOML | | pi | `~/.pi/agent/prompts/` | `.pi/prompts/` | Markdown + frontmatter | | zcode | `~/.zcode/commands/` | `.zcode/commands/` | Markdown + frontmatter | Agents not listed (antigravity, warp, replit, openclaw, trae, kiro-cli, goose) are skipped silently for commands. Continue Dev's `.prompt` files get an `invokable: true` preamble injected automatically, and Kasetto best-effort rewrites `$ARGUMENTS` to Continue's `{{{ input }}}` placeholder. ## Stale-File Safety Kasetto only deletes files it claims to own in the lock file. Pre-existing user-authored files in the same directory are preserved even if a command is removed from your config. --- # Sharing Instructions Across Repos Keep one CLAUDE.md, AGENTS.md, and .cursor/rules in sync across every repository and agent, without copy-paste or symlinks. **When you need this:** You have coding conventions in a `CLAUDE.md`, `AGENTS.md`, or `.cursor/rules` and you want the same rules in every repository, on every machine, for every agent you use. ## The Drift Problem Instruction files are per-repository by design. Once you work in more than one repo, the copies diverge: you fix a rule in one project's `CLAUDE.md` and the other four keep the old wording. Add a second agent and it doubles, because Cursor wants `.cursor/rules/*.mdc` while Claude Code wants `CLAUDE.md` and Codex wants `AGENTS.md`. Symlinking a shared file breaks down quickly. It cannot merge with the repo-specific content that belongs in the same file, it does not survive a fresh clone, and it has no notion of a version. Kasetto treats instructions as a synced asset kind, like skills and MCP servers. You write each rule once, and `kst sync` renders it into whatever shape each agent expects. ## Publish Instructions in a Repo An instruction is Markdown with optional YAML frontmatter, living in the source's `instructions/` directory. Any Git repository works - a dedicated one, or a folder inside a repo you already have. ```markdown --- description: Comment punctuation rules. globs: ["**/*.rs", "**/*.ts"] alwaysApply: false --- Code comments never end with a period. Docstrings keep normal sentence punctuation. ``` Nested directories namespace with `:`, so `instructions/house/security.md` is referenced as `house:security`. ## Consume Them Point a `kasetto.yaml` at the source and name the instructions you want: ```yaml agent: - claude-code - cursor - codex instructions: - source: https://github.com/your-org/agent-conventions instructions: - comment-punctuation - house:security ``` Then sync: ```bash kst sync ``` Or skip the hand-editing and let the CLI write the config for you: ```bash kst add https://github.com/your-org/agent-conventions --instruction comment-punctuation ``` Use `instructions: "*"` to take everything the source publishes. ## What Lands Where One instruction becomes a different artifact per agent. There are two destination shapes: | Shape | Example agents | Result | | --- | --- | --- | | Aggregate file | Claude Code, Codex, Gemini CLI, Copilot | All instructions merge into one shared `CLAUDE.md` / `AGENTS.md` / `GEMINI.md` | | Per-instruction directory | Cursor, Windsurf, Cline, Roo Code | One file per instruction, e.g. `.cursor/rules/comment-punctuation.mdc` | Cursor is the only agent that gets reconstructed `description` / `globs` / `alwaysApply` frontmatter; the rest receive the Markdown body. The full per-agent table is in [supported agents](/docs/agents#instruction-destinations). ## Your Own Edits Survive Aggregate files are shared with content you wrote yourself, so Kasetto wraps each instruction it installs in a managed comment block: ```markdown Code comments never end with a period. Docstrings keep normal sentence punctuation. ``` Everything outside those markers is left byte-for-byte alone. `kst remove` and `kst clean` strip only Kasetto's own blocks and never delete the file. Run `kst sync --dry-run` the first time you point Kasetto at an existing `CLAUDE.md`. It reports every file it would touch without writing anything. ## Project or Global Scope decides whether the rules apply to one repository or to everything you open: ```bash kst sync --project # writes ./CLAUDE.md, ./.cursor/rules/ kst sync --global # writes ~/.claude/CLAUDE.md, ~/.cursor/rules/ ``` Org-wide conventions usually belong in global scope, with repo-specific rules layered on top in project scope. Configs compose with `extends`, so a team base config can carry the shared set while each project adds its own - see [extending another config](/docs/configuration#extending-another-config). ## Updating and Rolling Back A plain `kst sync` installs exactly what `kasetto.lock` pins. To pick up new wording from a moving branch: ```bash kst sync --update ``` Pin `ref:` to a tag or commit when you want a rule set frozen, and commit `kasetto.lock` so everyone on the team renders the same instructions. To roll a change back, move the `ref:` and sync again. ## Auditing and Removing See what is installed: ```bash kst list --type instructions ``` `kst doctor` additionally reports orphaned instruction blocks left in a shared `CLAUDE.md` - useful after someone hand-edits a file Kasetto manages. To drop one: ```bash kst remove https://github.com/your-org/agent-conventions --instruction house:security ``` ## Next Steps - [Configuration reference](/docs/configuration#instruction-source-fields) - every field on an instruction source - [Supported agents](/docs/agents#instruction-destinations) - exact destination for all 23 agents - [Cookbook](/docs/cookbook) - team, monorepo, and multi-agent setups --- # How Sync Works How kst sync resolves sources, hashes content, diffs against kasetto.lock, and writes only what actually changed. A look at what `kst sync` actually does: how skills are installed, how MCP configs are merged, what gets overwritten, and what's always left alone. ## Sync Flow ``` 1. Load config (`$KASETTO_CONFIG` → `./kasetto.yaml` → `source:` in `$XDG_CONFIG_HOME/kasetto/config.yaml` → `$XDG_CONFIG_HOME/kasetto/kasetto.yaml`, or explicit `--config` path/URL) 2. Load lock file (kasetto.lock) 3. Sync skills a. For each skill source: decide whether a fetch is needed (the lock pins it and the destinations already match → no network) b. Materialize (download if remote) only when a fetch is needed c. Discover skills in source directory d. For each skill: hash → compare to lock → copy if changed e. Remove skills no longer in config 4. Sync commands a. For each command source: decide whether a fetch is needed (the lock pins it and the destination files already exist → no network) b. Materialize (download if remote) only when a fetch is needed c. Discover commands in source, transform into each agent's native format d. For each command: hash → compare to lock → write if changed e. Remove commands no longer in config 5. Sync MCPs a. For each MCP source: decide whether a fetch is needed (the lock pins it and its servers are already present in the agent settings → no network) b. Materialize (download if remote) only when a fetch is needed c. Discover MCP pack files in source d. For each pack: hash → compare to lock → merge into agent settings if changed e. Remove MCP entries no longer in config 6. Save lock file and report (unless --dry-run) ``` ## Skills: Copy and Replace Skills are plain directories with a `SKILL.md` file (see [writing skills](/docs/writing-skills)). On each sync, Kasetto: - **Resolves** the source root (optionally via `sub-dir`) - **Discovers** skills from: - top-level `SKILL.md` in the resolved root, - root-level subdirectories with `SKILL.md`, and - `skills//SKILL.md` - **Hashes** the source skill directory - **Compares** the hash to the lock file. If unchanged, the skill is skipped - **Copies** the entire directory to the destination, replacing any previous version (including symlinked files/directories inside the skill) - **Removes** skill directories that are no longer listed in the config Skills are fully replaced on update. There are no partial merges. ## MCP Servers: Discovery and Additive Merge Kasetto auto-discovers MCP pack files in the source: 1. `.mcp.json` at the source root 2. `mcp.json` at the source root 3. Any `.json` file inside the `mcps/` subdirectory Each file must contain a `mcpServers` JSON object. Use a `mcps:` list to pick specific files instead of discovering all. Server entries are merged into each selected agent's native settings file (e.g., `.claude.json`, `.cursor/mcp.json`) when that agent supports MCP. Pi intentionally has no native MCP support, so Kasetto skips MCP sources for the `pi` preset. The merge follows two rules: 1. **New entries are added.** If the settings file doesn't have a server with that name, it's inserted 2. **Existing entries are never overwritten.** If a server name already exists, whether added by Kasetto or by hand, Kasetto leaves it untouched In practice, this means: - **Manual edits are safe.** If you add API keys, environment variables, or tweak server settings after a sync, those changes survive future syncs - **Re-sync is idempotent.** Running `sync` twice with the same config produces the same result - **First write wins.** If two sources define a server with the same name, only the first one is installed ### Supported Config Formats Kasetto auto-detects the right format for each agent: | Format | Used by | Target file example | | -------------------- | ------------------------------------------------ | ---------------------------------- | | McpServers JSON | Claude Code, Cursor, Gemini CLI, Roo, and others | `.claude.json`, `.cursor/mcp.json` | | VS Code servers JSON | GitHub Copilot | `.vscode/mcp.json` | | OpenCode JSON | OpenCode | `.config/opencode/opencode.json` | | Codex TOML | Codex | `.codex/config.toml` | | ZCode JSON | ZCode (nested `mcp.servers`) | `.zcode/cli/config.json` | All formats follow the same additive-merge rules. ## Change Detection Kasetto uses SHA-256 hashes so it only does work when something actually changed: - **Skills:** The entire skill directory is hashed. If the hash matches the lock file, the skill is skipped - **MCP packs:** The pack file is hashed. If the hash matches **and** all server names are still present in the target settings, the pack is skipped This keeps re-sync fast: unchanged sources require no file I/O beyond reading the lock. ## The Lockfile Contract `kasetto.lock` is a real lockfile, in the same spirit as `Cargo.lock`, `package-lock.json`, or `go.sum`. It pins exactly which skills, commands, instructions, and MCP assets were installed (by source, revision, destination, and content hash), so anyone who runs `kst sync` against the same config and lock gets an identical setup. Where it lives depends on the scope: | Scope | Location | | ------- | -------------------------------------------------------------------------------------- | | Global | `$XDG_DATA_HOME/kasetto/kasetto.lock` (default: `~/.local/share/kasetto/kasetto.lock`) | | Project | `./kasetto.lock` in the project root | The lock is also how Kasetto knows what to remove when you drop a source from the config or run `kst clean`. You generally won't need to touch it by hand. **Skills** are tracked by a composite key (`source::skill-name`) along with their destination path and hash. **MCPs** are tracked as assets with the server names they installed (e.g., `destination: "git-tools,airflow"`). Only these tracked names are removed during cleanup. Kasetto never touches entries it didn't install. Manually added servers, skills from other tools, or entries from a different scope are always left alone. ### Commit It For project scope, commit **both** `kasetto.yaml` and `kasetto.lock`. The config says *what* you want; the lock says *exactly which versions* everyone gets. The lock is deterministic and portable by design: - Destination paths are stored **relative to the scope root**, so the file is identical on every machine and diffs cleanly in review - It contains **no timestamps or machine-specific data**, so nothing run-specific leaks in Machine-local runtime state (last-run report, per-skill install times used by `kst list` and `kst doctor`) lives separately under `$XDG_CACHE_HOME/kasetto/runtime/` and is **never committed**. It is safe to delete and regenerates on the next sync. A custom **absolute** `destination:` stays non-portable by design. An absolute path can't be relativized, so it's stored as-is. Use a relative `destination:` (or the defaults) if you want the lock to travel between machines. ### How `sync` Honors the Lock | Command | Behavior | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `kst sync` | Installs exactly what the lock pins. If the on-disk destinations already match the locked hashes, **no network fetch happens**, and moving refs like a branch are not re-resolved. | | `kst sync --update` (`-u`) | Re-resolves branch / default-HEAD sources, downloads the latest, and rewrites the pins (hash + revision) in the lock. | | `kst sync --update ` | Updates only the selected entries; everything else is honored from the lock. (Naming one skill re-resolves its whole source.) | | `kst sync --update-local` | Re-resolves every **local-path** source (a `source:` that is a directory, not a URL) and rewrites its pins, no names needed. Remote sources are untouched. | | `kst sync --locked` / `--frozen` | Strict mode for CI: installs from the lock and **never fetches**. Errors only if the config needs something the lock can't satisfy. | A plain `kst sync` always re-hashes the destination contents locally (cheap, no network) and repairs any tampered or missing copy from the lock, but it will not phone home to check whether an upstream branch moved. That is what `--update` is for. `--update` and `--locked` together are contradictory and rejected; so are `--update-local` and `--locked`. Local directory sources are pinned by the lock exactly like remote ones: edit a skill under `./skills/` and a plain sync still serves the locked copy. `--update-local` is the escape hatch scoped to just those sources. It re-reads every local directory and rewrites its pins while leaving remote branch pins alone, so shipping your own edits never drags in upstream churn. Combine it with `--update ` to refresh a named remote in the same run. ### Wildcards Are Frozen to the Locked Set When a source uses `skills: "*"`, a plain `kst sync` installs exactly the set recorded in the lock, not whatever the source currently contains. New skills added upstream and skills removed upstream are only reflected after `kst sync --update`, which re-discovers the source and prunes accordingly. This keeps the lock authoritative: the wildcard resolves the same way for every teammate until someone deliberately updates. **One-time `updated` flood after upgrading.** A release normalizes the skill content hash across operating systems (path separators are unified before hashing). The first `kst sync` after upgrading will therefore show every skill as `updated` once as it rewrites the lock with the new hash format. This is expected and self-heals: subsequent syncs report `unchanged` again. ## Removal Behavior ### Removing a Source from Config Remove a source from `kasetto.yaml` and re-sync: - **Skills:** The skill directory is deleted from disk - **MCPs:** The specific server entries that Kasetto installed are removed from the agent's settings file. The file itself is preserved ### `kst clean` Removes everything tracked in the lock file for the given scope: - Deletes all tracked skill directories - Removes all tracked MCP server entries from every agent's settings file - Clears the lock file `--dry-run` prints what would be removed (skill destinations and MCP pack lines) without changing disk. Anything you added by hand outside the lock is never touched. ## Edge Cases **Conflicting server names across sources.** If source A and source B both define a server named `"my-server"`, source A wins (it's processed first). If you later remove source A, `"my-server"` gets removed, even though source B also wanted it. Re-sync will then install source B's version. **Renamed servers in upstream.** If an upstream MCP pack renames a server from `"old"` to `"new"`, Kasetto treats it as: remove `"old"` (no longer in pack) + add `"new"` (not yet in settings). The old entry is cleaned up and the new one is added. **Corrupted settings file.** If an agent's settings file is malformed JSON/TOML, the merge for that file fails and is reported as an error. The lock file isn't updated for that pack, so the next sync retries the merge automatically. --- # Sync Flow Diagrams Diagrams of how kst sync resolves sources, discovers assets, diffs the lock, and writes into each agent environment. Complete reference for how `kst sync` resolves sources, discovers skills, commands, instructions, and MCP files, diffs against the lock, and writes to agent environments. --- ## Top-Level Pipeline ```mermaid flowchart TD A[kasetto.yaml] --> B[Resolve scope & destinations] B --> C[skills loop] B --> D[mcps loop] C --> C1[Materialize source] C1 --> C2[Discover available skills] C2 --> C3[Select targets] C3 --> C4[Hash & diff vs lock] C4 --> C5[Copy to destinations] C5 --> C6[Update lock state] D --> D1[Materialize source] D1 --> D2[Resolve MCP files] D2 --> D3[Parse mcpServers] D3 --> D4[Hash & diff vs lock] D4 --> D6[Merge into agent settings] D6 --> D7[Update lock state] C6 --> E[Save lock file + report] D7 --> E ``` --- ## Source Materialization Shared by all asset kinds: skills, commands, instructions, and MCPs. ```mermaid flowchart TD A[source: field] --> B{Remote or local?} B -->|URL| C[Resolve auth token from env vars] C --> D[Rewrite browser URL to raw endpoint if needed] D --> E{Git pin} E -->|ref: set| F[Pin to tag / SHA, no fallback] E -->|branch: set| G[Pin to explicit branch, no fallback] E -->|neither| H[Try main → fall back to master] F --> I[Download tarball → extract to temp dir] G --> I H --> I I --> J{sub-dir set?} J -->|yes| K[Use sub-dir as root] J -->|no| L[Use repository root] B -->|local path| M[Expand ~ and resolve relative to config] M --> J K --> N[root directory on disk] L --> N ``` --- ## Skills Sync Flow ### Discovery ```mermaid flowchart TD A[root directory] --> B{root/SKILL.md exists?} B -->|yes| C[Root itself is a skill\nnamed after repository / dir] B -->|no| D[No root skill] C --> E[Scan root/ for subdirs with SKILL.md] D --> E E --> F[Scan root/skills/ for subdirs with SKILL.md] F --> G[available map: name → path] ``` ### Target Selection ```mermaid flowchart TD A[skills field] --> B{Type?} B -->|Wildcard '*'| C[All entries in available map] B -->|List| D[For each entry] D --> E{Entry form?} E -->|Name string| F{name in available?} F -->|yes| G[Use available path] F -->|no| H[broken: skip, report] E -->|Object name + path| I{path/name/SKILL.md exists?} I -->|yes| J[Use explicit path] I -->|no| K{name in available?} K -->|yes| L[Use available path] K -->|no| M[broken: skip, report] C --> N[targets list] G --> N J --> N L --> N ``` ### Hash, Diff & Copy ```mermaid flowchart TD A[skill target] --> B{key in lock?} B -->|no| C[is_new = true] B -->|yes| D{hash matches\nAND dest exists?} D -->|yes| E[unchanged: skip] D -->|no| F[queue as update] C --> G{dry-run?} F --> G G -->|yes| H[report would_install / would_update] G -->|no| I[copy_dir to every agent destination] I --> J[write lock entry\nhash + dest + revision + timestamp] ``` ### Stale Removal ```mermaid flowchart TD A[lock entries] --> B{key in desired set?} B -->|yes| C[keep] B -->|no| D{dry-run?} D -->|yes| E[report would_remove] D -->|no| F[remove_dir_all destination] F --> G[remove from lock] ``` --- ## MCP Sync Flow ### File Resolution ```mermaid flowchart TD A[mcps field] --> B{Value?} B -->|Wildcard '*'| C[discover_mcps root] C --> C1{root/.mcp.json exists?} C1 -->|yes| C2[include .mcp.json] C1 -->|no| C3[skip] C2 --> C4{root/mcp.json exists?} C3 --> C4 C4 -->|yes| C5[include mcp.json] C4 -->|no| C6[skip] C5 --> C7[include all *.json in root/mcps/] C6 --> C7 B -->|List| D[For each entry] D --> E{Entry form?} E -->|Name string e.g. github| F[root/mcps/github.json\nauto-append .json if missing] E -->|Object with path| G[root/path/name.json\ndefault dir is mcps/] C7 --> H[resolved file list] F --> H G --> H ``` ### Parse, Hash & Diff ```mermaid flowchart TD A[resolved .json file] --> B{Contains mcpServers object?} B -->|no| C[broken: skip, report] B -->|yes| D[extract server names] D --> E{asset_id in lock?} E -->|no| F[is_new = true] E -->|yes| G{hash matches AND\nall servers in settings?} G -->|yes| H[unchanged: skip] G -->|no| I[is_new = false, queue as update] F --> J[pending list] I --> J ``` ### Merge into Agent Settings ```mermaid flowchart TD A[server definition] --> B{Agent format?} B -->|McpServers JSON| C[write under mcpServers key\nClaude Code, Cursor, Roo,\nCline, Gemini CLI, Goose,\nWindsurf, Amp and others] B -->|VsCodeServers JSON| D[write under servers key\nadd type: stdio if missing\nGitHub Copilot / VS Code] B -->|OpenCode JSON| E[convert to type:local or type:remote\nwrite under mcp key\nOpenCode] B -->|CodexToml TOML| F[write mcp_servers.name table\nCodex] C --> G{server name already\nin target file?} D --> G E --> G F --> G G -->|yes| H[skip: never overwrite] G -->|no| I[write server definition] I --> J[save settings file] ``` --- ## Scope & Destinations ```mermaid flowchart TD A[Resolve scope] --> B{Source?} B -->|--project / --global flag| C[use CLI value] B -->|scope: in config| D[use config value] B -->|neither| E[default: global] C --> F{Scope} D --> F E --> F F -->|global| G[~/.agent/skills/ per agent\nlock: ~/.local/share/kasetto/kasetto.lock] F -->|project| H[./.agent/skills/ per agent\nlock: ./kasetto.lock] ``` --- ## Dry Run `--dry-run` skips all writes. Actions report `would_install`, `would_update`, `would_remove`. Lock file is never modified. --- # Cookbook Copy-paste Kasetto setups for teams, monorepos, multiple agents, and pinned rollouts. **When you need this:** You want copy-paste setups for common real workflows (teams, monorepos, multiple agents, pinned rollouts). **What you'll learn:** - Patterns that work well in practice - How to pin and roll out changes safely ## Reproducible Team Setup (Commit the Lock) Treat `kasetto.lock` like `Cargo.lock` or `package-lock.json`: commit it next to `kasetto.yaml` so every teammate gets identical skill versions. ```bash # Maintainer: set up the config, sync, then commit both files. kst sync --project git add kasetto.yaml kasetto.lock git commit -m "chore: pin agent skills" ``` ```bash # Teammates: clone, then sync. The lock is honored exactly: # no surprise upgrades, and no network fetch when nothing changed. kst sync ``` ```bash # Maintainer rolls versions forward, then commits the updated lock. kst sync --update # re-resolve branches/HEAD, rewrite pins # or update one entry: kst sync --update code-reviewer git add kasetto.lock git commit -m "chore: bump agent skills" ``` ```bash # CI verifies the checked-in lock without ever fetching new versions. kst sync --locked --project ``` `--locked` (alias `--frozen`) errors if the config needs something the lock can't satisfy, so a stale lock fails the build instead of silently drifting. See [CI & automation](/docs/ci) and [How Sync Works → The Lockfile Contract](/docs/how-sync-works#the-lockfile-contract). ## Edit Your Config Without Leaving the Shell `kst add` / `kst remove` rewrite `kasetto.yaml` (comments preserved), then sync (the cargo/uv way). Paste a tag-pinned URL with the `@` shorthand and you're done: ```bash # Pin a pack to a specific tag. The `@` shorthand equals `--ref `. kst add https://github.com/anthropics/skills@v1.2.0 # Add named skills from a multi-pack repo (kind-tagged, repeatable). kst add https://github.com/example/repo --skill code-reviewer --skill find-skills # Touch several sections in one edit: skills + MCPs + commands + instructions together. kst add https://github.com/example/repo --skill find --mcp github --command review --instruction conventions # Preview the edit without writing kasetto.yaml or running sync. kst add https://github.com/example/repo --dry-run # Edit the config but skip the install (sync later or in CI). kst add https://github.com/example/repo --no-sync ``` `kst remove` is the symmetric reverse: same kind-tagged flags, with `*` to drop a whole entry: ```bash # Drop the source from every list it appears in, prune installed assets. kst remove https://github.com/example/repo # Subtract just one skill; if it was the last name, the whole entry is dropped. kst remove https://github.com/example/repo --skill code-reviewer # Drop the whole MCP entry (the lone `*` means "this kind's entry"). kst remove https://github.com/example/repo --mcp "*" # Same `@` shorthand for disambiguating when one URL appears twice. kst remove https://github.com/example/repo@v1.2.0 ``` Both honor `--locked`/`--frozen` (the follow-up sync refuses to fetch), `--json` (structured output for scripts), and `--no-sync` (edit the YAML only). Deep `blob`/`tree` browse URLs work too. Paste the URL you're already looking at and Kasetto decomposes it into `source` + `ref`/`branch` + `sub-dir`. ## Verify the Lock in CI Without Installing `kst lock --check` (aliases `--locked`/`--frozen`) re-resolves the config and compares against `kasetto.lock`. It exits non-zero on drift and never writes. Cheaper than a full `sync --locked` when you only need to know "is the committed lock still accurate?": ```bash kst lock --check ``` When a single dependency needs to roll forward without re-resolving everything, target it with `-P` / `--upgrade-package` (mirrors `sync --update ...`): ```bash kst lock --upgrade-package code-reviewer ``` Other sources keep their existing lock entries; only the source providing `code-reviewer` is re-resolved. ## Team Bootstrap from a URL Config Host a shared `kasetto.yaml` somewhere reachable over HTTPS (public or private), then have each developer run: ```bash kst sync --config https://example.com/team/kasetto.yaml ``` For private configs hosted on git providers, set the matching token env var (see [Authentication](/docs/authentication)). ## Inheriting from a Team or Org Base Use `extends` to compose configs. A common pattern: an org-wide base, a team overlay, and a per-project file that narrows or pins specific entries. ```yaml # project/kasetto.yaml extends: - https://github.com/acme/skills-base/raw/main/kasetto.yaml - https://example.com/team/overlay.yaml scope: project skills: # Same source as the base → narrows the parent's skills list to one entry. - source: https://github.com/anthropics/skills skills: - skill-creator # New source → appended on top of the inherited list. - source: https://github.com/acme/internal-pack skills: "*" ``` Top-level scalars (`scope`, `agent`, `destination`) replace. `skills` and `mcps` merge by `(source, ref-or-branch, sub-dir)` identity. See [Configuration → Extending Another Config](/docs/configuration#extending-another-config) for the full merge-rules table. ## Monorepo: Project Scope per Workspace Keep one `kasetto.yaml` per workspace folder and make it project-scoped: ```yaml scope: project agent: cursor skills: - source: https://github.com/acme/monorepo-skills skills: - code-reviewer - doc-coauthoring ``` Then run sync from each workspace directory: ```bash kst sync ``` Each workspace gets its own `./kasetto.lock`. ## Multiple Agents from One Config Install the same skills (and MCPs) into multiple agents: ```yaml agent: - claude-code - cursor - codex skills: - source: https://github.com/acme/skills skills: "*" mcps: - source: https://github.com/acme/mcp-packs mcps: "*" ``` ## MCP Packs: Pinning and Rollouts Pin an MCP pack source to a git tag or commit SHA: ```yaml agent: claude-code skills: - source: https://github.com/acme/skills skills: "*" mcps: - source: https://github.com/acme/mcp-packs ref: v2.4.1 mcps: "*" ``` Roll forward by bumping `ref`, then use `--dry-run` to preview: ```bash kst sync --dry-run ``` ## Explicit MCP Entries (`mcps.mcps`) If a repository contains multiple MCP files or uses a non-standard layout, list entries explicitly. Plain strings look up `mcps/.json`; objects let you override the directory: ```yaml mcps: # Names resolved from mcps/ dir (auto .json extension) - source: https://github.com/acme/monorepo ref: v1.4.0 mcps: - github # → mcps/github.json - linear # → mcps/linear.json # Custom directory via { name, path } - source: https://github.com/acme/other mcps: - name: my-server path: tools # → tools/my-server.json ``` --- # CI & Automation Validate kasetto.yaml in CI, catch config drift with --frozen, and keep team AI environments reproducible. **When you need this:** You want to validate `kasetto.yaml` in CI, keep team environments reproducible, or integrate Kasetto into scripts. **What you'll learn:** - Recommended flags (`--dry-run`, `--json`, `--color never`, `--quiet`) - Exit-code expectations - A GitHub Actions example ## Recommended Commands ### Enforce the Locked Versions If you commit `kasetto.lock` (recommended for teams, see [Cookbook](/docs/cookbook#reproducible-team-setup-commit-the-lock)), use `--locked` to install exactly what the lock pins and **never fetch new versions**: ```bash kst sync --locked ``` `--locked` (alias `--frozen`) errors if the config needs something the lock can't satisfy, so a stale or out-of-sync lock fails the build instead of drifting. It still repairs tampered destinations locally, but never resolves moving refs or downloads new content. ### Validate Without Writing Use `--dry-run` to check that sources resolve and that the plan matches expectations without touching disk: ```bash kst sync --dry-run ``` ### JSON Output for CI Logs Use `--json` for structured logs: ```bash kst sync --dry-run --json ``` ### Strip Colors and Animations Kasetto auto-detects non-TTY output and emits plain text, but you can force it: - `--color never` (preferred) - `-q` / `--quiet` to suppress non-error output entirely - The `NO_COLOR` env var is honored ```bash kst sync --dry-run --color never ``` ## Exit Codes Kasetto is designed to keep going when individual skills are missing/broken in a source, but failures that prevent reading sources/configs are treated as errors. If you're depending on strict enforcement in CI, pair `--dry-run` with `--json` and enforce policy in the CI step based on the report. ## GitHub Actions Example This validates a repository's `kasetto.yaml` (project scope) without writing changes: ```yaml name: kasetto on: pull_request: push: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install kasetto run: curl -fsSL kasetto.dev/install | sh - name: Validate kasetto.yaml env: # Add tokens if you pull from private sources: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: kst sync --project --dry-run --json ``` --- # Security Model What Kasetto can write to disk, how it handles credentials, and whether syncing skills from a URL is safe. **When you need this:** You want to know what Kasetto can modify on disk, how it handles credentials, and whether syncing from URLs is safe. **What you'll learn:** - What Kasetto touches (and what it avoids) - How tokens are provided (env vars only) - How remote config fetching is authenticated ## What Kasetto Changes on Disk During `kst sync`, Kasetto may: - **Install/update skills** by copying skill directories into the chosen destination path - **Remove skills** that are no longer in your config (for the selected scope) - **Merge MCPs** into agent-native settings files (additive merge; existing servers are preserved, except a kasetto-managed server replaced by `--update`) - **Write the lock file** for the selected scope Kasetto is designed around a "tracked-only" principle: - **Skills**: fully managed at their install paths for entries tracked in the lock - **MCPs**: only server entries that Kasetto installed (tracked in the lock) are removed during cleanup See [How Sync Works](/docs/how-sync-works) for details. ## MCP Server Installation New MCPs are merged into agent settings files automatically on `kst sync`, with no confirmation prompt. Use `--dry-run` to preview what would be written before committing. ## Self-Update Integrity `kst self update` verifies the downloaded binary against `checksums.txt` from the same GitHub release using SHA256, the same verification the shell installer (`install.sh`) performs. If the checksum doesn't match, the update is aborted and the existing binary is left untouched. ## What Kasetto Does Not Do - It does not run skill code - It does not overwrite MCP server entries it didn't install (only a Kasetto-managed server is replaced, and only under `--update`) - It does not write a credentials file. It can optionally *read* one for [secret injection](/docs/secrets) ## Secret Injection MCP packs can carry `${kst_...}` placeholders that Kasetto resolves at sync time from environment variables, `$XDG_CONFIG_HOME/kasetto/credentials.yaml`, or an external manager via the tagged form: `op` (1Password), `vault` (HashiCorp Vault), `kp` (KeePassXC), `aws` (Secrets Manager), `gcp` (Secret Manager), `az` (Key Vault), `pass` (pass/gopass), `keychain` (macOS). Each shells out to the provider's own CLI and inherits your existing session, so Kasetto stores no tokens. The resolved value is written **only** to the agent's settings file, never to `kasetto.lock` (which hashes the placeholder source), the source cache, or the stage directory. A missing required secret fails the entry and exits non-zero by default. See [Secret Injection](/docs/secrets) for the full model. ## Credentials and Tokens Kasetto reads tokens from environment variables (per host). Examples: - GitHub / GitHub Enterprise: `GITHUB_TOKEN` or `GH_TOKEN` - GitLab / self-hosted GitLab: `GITLAB_TOKEN` or `CI_JOB_TOKEN` - Bitbucket Cloud: `BITBUCKET_EMAIL` + `BITBUCKET_TOKEN` (or app password variants) See [Authentication](/docs/authentication) for the full list and host detection rules. ## Remote Config Fetching (`--config https://...`) When you pass a URL to `--config`, Kasetto: - Fetches the YAML over HTTPS - Applies the same host-based token selection rules as skill/MCP sources This means a private config hosted on a git provider can be accessed by setting the appropriate token env var for that host. ## Practical Recommendations - Prefer pinning remote sources to immutable refs (`ref: v1.2.3` or a commit SHA) for stable rollouts - In CI, use `--dry-run` (and ideally `--json`) to validate without writing changes --- # FAQ Common questions about syncing skills, MCP servers, and instructions across AI coding agents. **When you need this:** You have a quick "what happens if..." question about syncing skills or MCPs. ## Will Kasetto Overwrite My MCP Entries? No. MCP merges are **additive** and existing server entries are **never overwritten**. See [How Sync Works](/docs/how-sync-works#mcp-servers-discovery-and-additive-merge). ## What Happens When Two Sources Define the Same MCP Server Name? **First write wins** based on config order. Later sources with the same server name are skipped. See [How Sync Works](/docs/how-sync-works#edge-cases). ## Where Is the Lock File? - **Global scope**: `$XDG_DATA_HOME/kasetto/kasetto.lock` (default: `~/.local/share/kasetto/kasetto.lock`) - **Project scope**: `./kasetto.lock` See [How Sync Works](/docs/how-sync-works#the-lockfile-contract). ## Should I Commit `kasetto.lock`? Yes. For project scope, commit **both** `kasetto.yaml` and `kasetto.lock`, just like you'd commit `Cargo.lock` or `package-lock.json`. The config says what you want; the lock pins exactly which versions everyone gets. The lock is deterministic and portable (relative paths, no timestamps), so it diffs cleanly. Machine-local runtime state lives in the cache dir and is never committed. See [How Sync Works → The Lockfile Contract](/docs/how-sync-works#the-lockfile-contract). ## How Do I Update Pinned Versions? Run `kst sync --update` (alias `-u`) to re-resolve branch / default-HEAD sources and rewrite the pins. A plain `kst sync` honors the lock and won't fetch when destinations already match. Use `kst sync --update ` to update only selected entries, or `kst sync --update-local` to re-resolve every local directory source (the usual way to ship your own skill edits without touching remote pins). See [How Sync Works → How `sync` Honors the Lock](/docs/how-sync-works#how-sync-honors-the-lock). ## How Do I Uninstall Safely? - To remove tracked installs for a scope: `kst clean` - To remove everything including the binary: `kst self uninstall` See `kst clean` and `kst self uninstall` in [Commands](/docs/commands). ## Can I Use Multiple Agents? Yes. Set `agent` to a list. See [Configuration](/docs/configuration#multiple-agents). ## Does Kasetto Run Code from Skills? No. Skills are copied as directories. Execution is up to the agent you load them into. ## Can I Pin Sources to a Known-Good Version? Yes. Use `ref:` with a tag or commit SHA. See [Cookbook](/docs/cookbook#mcp-packs-pinning-and-rollouts). ## What Does `--dry-run` Do? It prints what would change without writing any files. Useful in CI. See [CI & automation](/docs/ci). ## Why Didn't My MCP Servers Show Up? Most common causes: - The target agent settings file is malformed JSON/TOML - The MCP file doesn't contain a top-level `mcpServers` object See [How Sync Works](/docs/how-sync-works#edge-cases) (corrupted settings file behavior). ## How Do Remote Configs (`--config https://...`) Authenticate? Kasetto selects tokens by the URL hostname using the same rules as skill/MCP sources. See [Authentication](/docs/authentication#remote-configs).