KASETTO

How Sync Works

Internals of the sync process.

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` → saved preference → `$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). 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/<name>/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 — 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 agent's native settings file (e.g., .claude.json, .cursor/mcp.json). The merge follows two simple 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:

FormatUsed byTarget file example
McpServers JSONClaude Code, Cursor, Gemini CLI, Roo, and others.claude.json, .cursor/mcp.json
VS Code servers JSONGitHub Copilot.vscode/mcp.json
OpenCode JSONOpenCode.config/opencode/opencode.json
Codex TOMLCodex.codex/config.toml

All formats follow the same additive-merge rules — the underlying behavior is identical.

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, and MCP assets were installed — by source, revision, destination, and content hash — so that anyone who runs kst sync against the same config and lock gets an identical setup.

Where it lives depends on the scope:

ScopeLocation
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 — 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

CommandBehavior
kst syncInstalls exactly what the lock pins. If the on-disk destinations already match the locked hashes, no network fetch happens — 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 <name>Updates only the selected entries; everything else is honored from the lock. (Naming one skill re-resolves its whole source.)
kst sync --locked / --frozenStrict 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.

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.

On this page