A package manager for AI agent skills.
Declare your skills and commands in one YAML file, and get the same setup on every machine.
Agent skills tend to accumulate in ~/.claude/ by hand: copied from a gist, tweaked in place,
and impossible to reproduce when you switch machines. kata turns that directory into
something you declare rather than something you accumulate.
Commit a kata.yml, a kata.lock, and your own skills under local/. On any other machine,
kata sync puts everything back — from the same commit, down to the same file.
$ kata sync
+ my-review skill ~/.claude/skills/my-review
+ pdf skill ~/.claude/skills/pdf
2 created, 0 updated, 0 removed, 0 unchanged- Reproducible.
kata.lockpins the exact commit, soref: mainstill resolves to the same tree on every machine. - Declarative.
kata syncconverges the deployed state onto the manifest and is safe to run repeatedly. Drop a package from the manifest and it gets undeployed. - Non-destructive. kata only removes what kata created. Anything you placed by hand is never
overwritten or deleted without
--force. - Bring your own skills. Sources can be a git repository (with a subdirectory), or a directory inside your own repo — so skills you write yourself are versioned alongside the manifest.
- Single binary. Pure Go, no runtime dependencies, builds for macOS, Linux and Windows.
- Agent-friendly. Every command has a
--jsonoutput, andkata mcpexposes kata's operations as MCP tools — see Using kata from an AI agent.
$ brew install --cask cutmail/tap/kataUpgrade later with brew upgrade --cask kata. Homebrew casks are macOS-only; on Linux use one of
the options below.
kata is not signed or notarized by Apple, and the cask does not clear the quarantine attribute for you — see Gatekeeper on macOS below.
Pre-built binaries for macOS, Linux and Windows (amd64 and arm64) are attached to every release.
$ curl -L https://github.com/cutmail/kata/releases/latest/download/kata_darwin_arm64.tar.gz | tar xz
$ sudo mv kata /usr/local/bin/$ go install github.com/cutmail/kata/cmd/kata@latest$ git clone https://github.com/cutmail/kata && cd kata
$ go build -o ~/bin/kata ./cmd/kataVerify the installation with:
$ kata --version
kata version 0.1.0Every release archive carries a GitHub-native SLSA build provenance attestation, proving the archive was built by this repository's release workflow from the tagged commit. Check it with the GitHub CLI:
$ gh attestation verify kata_darwin_arm64.tar.gz -R cutmail/katachecksums.txt is published in the same GitHub Release as the archives, so it only protects
against a corrupted download — anyone able to replace a release can replace its checksums too.
The attestation is what protects against a replaced release, so prefer it.
kata is not signed or notarized by Apple. macOS quarantines anything downloaded from the internet, so the first launch may be blocked with a message about an unidentified developer. This applies to the Homebrew cask as well: kata deliberately does not strip the quarantine attribute for you.
A package that silently removes quarantine removes your only warning that unsigned code is about
to run — and it does so on every brew upgrade, for every user, forever. Whether to trust an
unsigned binary is your call to make, once you have verified where it came from.
Verify the download first, then choose one of:
-
Right-click the binary in Finder and pick Open, then confirm. macOS remembers the choice.
-
Or clear the attribute yourself:
$ xattr -d com.apple.quarantine /usr/local/bin/kataWith Homebrew, the installed path is
$(brew --prefix)/bin/kata.
Create a repository for your agent configuration:
$ mkdir my-agent-config && cd my-agent-config
$ git init && kata init
created kata.yml
put your own skills under local/ and register them with 'kata add'Add a skill from a public repository:
$ kata add anthropics/skills --path skills/pdf --ref main
added pdf (skill) to kata.yml
+ pdf skill ~/.claude/skills/pdf
1 created, 0 updated, 0 removed, 0 unchangedAdd a skill you wrote yourself:
$ mkdir -p local/skills/my-review && $EDITOR local/skills/my-review/SKILL.md
$ kata add ./local/skills/my-review
added my-review (skill) to kata.yml
+ my-review skill ~/.claude/skills/my-reviewCheck what is deployed:
$ kata list
NAME TYPE STATUS PROFILES SOURCE DEST
pdf skill linked all git+https://github.com/anthropics/skills@f17010c ~/.claude/skills/pdf
my-review skill linked all local:./local/skills/my-review ~/.claude/skills/my-reviewAlready have a ~/.claude full of hand-made skills? Bring them in without disturbing anything:
$ kata import --dry-run
dry run: nothing was changed
+ my-review skill ~/.claude/skills/my-review -> ./local/skills/my-review
+ pr command ~/.claude/commands/pr.md -> ./local/commands/pr.md
skip pdf managed by kata (declared in ~/dotfiles)
2 to import, 1 skippedCommit and push:
$ git add -A && git commit -m "my agent setup" && git pushOn another machine, clone and sync — that's the whole story:
$ git clone <your-repo> && cd <your-repo>
$ kata sync| Command | Description |
|---|---|
kata init |
Create a kata.yml and a local/ directory |
kata add <source> |
Add a package to the manifest and deploy it |
kata sync |
Converge the deployed state onto the manifest (idempotent) |
kata list |
Show every declared package and its current state |
kata status |
Report only what is out of sync; exits 1 when anything is |
kata import |
Adopt entries already in ~/.claude into the manifest |
kata update [name...] |
Re-resolve floating refs and move the lock forward |
kata doctor |
Check the environment and explain anything that looks wrong |
kata prune |
Remove cached content nothing refers to any more |
kata remove <name> |
Remove a package from the manifest and undeploy it |
Every command accepts --json, which prints its result as JSON on stdout instead of the human-readable
text above — see Using kata from an AI agent.
kata init flags:
| Flag | Description |
|---|---|
--json |
Print {"path": "..."} instead of the confirmation text |
kata add flags:
| Flag | Description |
|---|---|
--type skill|command|agent |
Package type (inferred when omitted; agent must be explicit) |
--name <name> |
Package name (defaults to the last path element) |
--path <subdir> |
Subdirectory inside the repository or archive |
--ref <branch|tag> |
Branch or tag (defaults to the default branch) |
--url |
Treat the source as an archive URL rather than a git repository |
--scope user|project |
Where to deploy (defaults to user) |
--strategy link|copy|auto |
How to deploy (defaults to link) |
--profile <name> |
Profiles this package belongs to (repeatable) |
--no-sync |
Only update the manifest, do not deploy |
--json |
Print the added package (and sync result, unless --no-sync) as JSON |
kata sync flags:
| Flag | Description |
|---|---|
--dry-run |
Show what would change without touching anything |
--force |
Move an existing file into the backup directory before deploying |
--profile <name> |
Only deploy packages in this profile (defaults to $KATA_PROFILE) |
--prune |
Also undeploy packages the profile leaves out |
--adopt |
Take ownership of a copied destination whose contents already match |
--json |
Print the sync report as JSON |
kata list flags:
| Flag | Description |
|---|---|
--json |
Print {"items": [...]} instead of the table |
kata status flags:
| Flag | Description |
|---|---|
-q, --quiet |
Print nothing; report the result through the exit code only |
--json |
Print the summary as JSON |
kata import flags:
| Flag | Description |
|---|---|
--dry-run |
Show what would be imported without writing anything |
--adopt |
Move the originals aside and link to the copies under local/ |
--type <list> |
Only import these types (comma separated) |
--json |
Print the import report as JSON |
kata update flags:
| Flag | Description |
|---|---|
--dry-run |
Show which commits would move without writing the lock (still hits the network) |
--no-sync |
Update the lock but leave the deployment as it is |
--json |
Print the update report as JSON |
kata doctor flags:
| Flag | Description |
|---|---|
--strict |
Exit with 1 on warnings too, not just errors |
--json |
Print the report as JSON |
kata prune flags:
| Flag | Description |
|---|---|
--apply |
Actually remove the listed items (nothing is removed without it) |
--store / --staging / --state |
Which kinds to consider (defaults to store and staging) |
--older-than <dur> |
Only consider items older than this |
--json |
Print the prune report as JSON |
kata remove flags:
| Flag | Description |
|---|---|
--json |
Print the result as JSON |
The source argument accepts owner/repo, github.com/owner/repo, a full git URL, an archive URL
ending in .tar.gz/.tgz/.zip, or a path inside the manifest directory. Type and name are
inferred when you leave them out: a .md file becomes a command, a directory becomes a skill.
An agent is also a .md file, so it cannot be inferred — pass --type agent.
kata list reports one of linked, copied, missing, drifted, broken, or orphan, so you
can tell at a glance whether the machine still matches what you declared. kata status shows only
the entries that need attention and exits with 1 when there are any, which makes it usable in CI.
kata is meant to be driven by an agent directly, not only by a human typing commands. There are two ways to do that.
Every command accepts --json, which prints its result as JSON on stdout instead of the
human-readable text shown above:
$ kata add ./local/skills/pdf --json
{
"package": {
"name": "pdf",
"type": "skill",
"local": "./local/skills/pdf",
"scope": "user",
"strategy": "link"
},
"sync": {
"changes": [
{ "name": "pdf", "type": "skill", "action": "create", "dest": "/home/you/.claude/skills/pdf" }
],
"dry_run": false
}
}The exit code still reflects success/failure the same way it does without --json (for example
kata status --json still exits 1 when something is out of sync), so scripts that only check the
exit code keep working unchanged.
kata mcp runs kata itself as an MCP server over stdio, so an
agent can call kata's operations as tools instead of shelling out to the CLI. Register it with
Claude Code:
$ claude mcp add kata -- kata mcpEvery tool takes an optional dir argument used to locate kata.yml — the same way the CLI
locates it from the current directory — which defaults to the server process's own working
directory when omitted. This is a different feature from the "MCP server configuration merging"
roadmap item below: that one is about kata managing other tools' MCP config as a deployment
target; kata mcp is kata itself acting as an MCP server.
| Tool | Does | Notes |
|---|---|---|
kata_init |
kata init |
|
kata_list |
kata list |
read-only |
kata_status |
kata status |
read-only; a drifted result is not a tool error |
kata_doctor |
kata doctor |
read-only; works even without a kata.yml |
kata_add |
kata add |
deploys immediately unless no_sync is set |
kata_sync |
kata sync |
dry_run previews with no side effects at all |
kata_import |
kata import |
destructive only when adopt is set |
kata_update |
kata update |
dry_run still reaches the network to resolve refs |
kata_prune |
kata prune |
only deletes anything when apply is set |
kata_remove |
kata remove |
no dry-run — call kata_list first to confirm the target |
version: 1
defaults:
scope: user
strategy: link
# Reusable sources, referenced by packages via `from:`
sources:
anthropic:
git: https://github.com/anthropics/skills
ref: main
packages:
# A subdirectory of a shared source
- name: pdf
type: skill
from: anthropic
path: skills/pdf
# Pinned to a tag
- name: mcp-builder
type: skill
git: https://github.com/anthropics/skills
ref: v1.2.0
path: skills/mcp-builder
# An archive over HTTPS. The content digest is the pin.
- name: toolkit
type: skill
url: https://example.com/toolkit-1.4.0.tar.gz
sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
path: toolkit-1.4.0/skills/toolkit
# Your own skill, versioned in this repository
- name: my-review
type: skill
local: ./local/skills/my-review
# A single-file command
- name: pr
type: command
local: ./local/commands/pr.md
# A subagent definition
- name: reviewer
type: agent
local: ./local/agents/reviewer.md
# Only deployed when you ask for this profile
- name: work-notes
type: skill
local: ./local/skills/work-notes
profiles: [work]
# Copied into the repository's own .claude so the team shares it
- name: house-style
type: skill
local: ./local/skills/house-style
scope: project
strategy: copykata.lock records the resolved commit — or, for a url source, the content digest — for every
package and belongs in version control. sync treats the lock as the source of truth, which is
what makes a floating ref reproducible.
Because the lock wins, editing ref: or git: in the manifest has no effect on its own. That is
deliberate, but it is also invisible, so kata doctor reports the mismatch and points you at
kata update. A sha256: that disagrees with the lock is different: kata stops rather than
picking one, because a digest is a claim about integrity rather than a moving reference.
A package with no profiles: is always selected. kata sync --profile work narrows the run to
packages that list work, and leaves everything else exactly as it is — deployed packages stay
deployed, and every lock entry is kept, so pinning is never lost by narrowing. Pass --prune
if you also want the packages outside the profile undeployed.
Set KATA_PROFILE in your shell to make a machine default.
scope: project deploys into .claude/ next to kata.yml rather than ~/.claude. With
strategy: link those links hold absolute paths, so add .claude/ to .gitignore and let
kata sync recreate them. With strategy: copy the deployment is real content and can be
committed, which is how a team shares a directory.
strategy: auto picks link where symlinks work and copy where they do not — useful on Windows.
kata.yml ──┐
├─→ fetch git → cached in ~/.kata/store
kata.lock ─┘ local → used in place, straight from your repo
│
↓
~/.claude/skills/<name> symlink
~/.claude/commands/<name>.md
│
↓
~/.kata/state.json record of what kata deployed
- Links are created as a temporary link and then
renamed into place, so a swap is atomic and never leaves a half-applied state. ~/.kata/storeis a pure cache keyed by URL and commit. Delete it any time;kata syncrebuilds it.state.jsonis what makes removal safe: kata only ever undeploys entries it recorded itself.
| Variable | Default | Description |
|---|---|---|
KATA_HOME |
~/.kata |
Cache, state and backups |
CLAUDE_CONFIG_DIR |
~/.claude |
Deployment target |
If a deploy target is occupied by a real file or directory that kata did not create, sync
reports that package as failed and leaves your file exactly as it is:
$ kata sync
! handmade skill ~/.claude/skills/handmade
error: handmade: destination is occupied by a non-kata file: ~/.claude/skills/handmade
0 created, 0 updated, 0 removed, 2 unchanged, 1 failedPass --force to move it to ~/.kata/backups/<timestamp>/ first. Nothing is ever deleted
outright, and the sources themselves — including your local/ directory — are never modified.
With strategy: copy there is no symlink to prove ownership, so kata records a digest of what it
wrote and re-checks it before touching anything. If you edited the deployed copy, kata says so and
keeps your edit — --force moves it aside, and never deletes it:
$ kata sync
! house-style skill ~/.claude/skills/house-style
error: house-style: destination was modified after kata deployed it
0 created, 0 updated, 0 removed, 1 unchanged, 1 failedkata import follows the same rule from the other direction: by default it only copies into
local/ and writes the manifest, and never touches what is already in ~/.claude. Pass --adopt
when you want the originals moved aside and replaced with links.
kata prune removes nothing unless you pass --apply, only ever considers paths it constructed
itself inside ~/.kata, and treats a cache as live if any repository on the machine still
refers to it. It never touches ~/.kata/backups: those are your files, and deleting them is
left to you.
To report a security problem, see SECURITY.md — please do not open a public issue for one.
What the current release supports:
| Supported | |
|---|---|
| Types | skill, command, agent |
| Sources | git (with subdirectory), archive URL (tar.gz, tgz, zip), local |
| Strategies | link, copy, auto |
| Scopes | user (~/.claude), project (<repo>/.claude) |
| Selection | profiles |
| Targets | Claude Code |
A manifest that uses type: agent, url:, scope: project, strategy: copy or profiles:
cannot be read by kata 0.1.x, even though the manifest version is still 1.
- MCP server configuration merging (kata managing other tools' MCP config as a deployment
target — not to be confused with
kata mcp, which is kata itself acting as an MCP server; see Using kata from an AI agent) - Additional targets beyond Claude Code
- A published JSON Schema for
kata.yml
$ go test ./... # includes tests that hit the network
$ go test -short ./... # offline only
$ go vet ./...The layout follows the two interfaces the design is built around — source.Fetcher for where
content comes from, and target.Resolver for where it goes:
cmd/kata/ CLI
internal/manifest kata.yml parsing, normalization and validation
internal/lockfile kata.lock
internal/state record of deployed entries
internal/store content cache
internal/source fetchers (git, local)
internal/target destination resolution (Claude Code)
internal/linker symlink deployment
internal/app orchestration
Releases are cut by GoReleaser from a tag. Pushing a v* tag builds
the cross-platform archives, publishes them to GitHub Releases, and updates the Homebrew cask
in cutmail/homebrew-tap:
$ git tag -a v0.1.0 -m "v0.1.0" && git push origin v0.1.0Artifacts are attested with actions/attest-build-provenance, so every archive can be traced
back to the workflow run and commit that produced it.
Updating the tap needs a HOMEBREW_TAP_TOKEN secret. It must be a fine-grained personal
access token scoped to cutmail/homebrew-tap alone, with Contents: Read and write and nothing
else — a classic PAT with the repo scope would hand every repository the owner can reach to
anyone who obtains the token. See SECURITY.md.
To check the configuration without publishing anything:
$ goreleaser check
$ goreleaser release --snapshot --cleanIssues and pull requests are welcome. Please run go test ./... and gofmt -l . before opening
a PR. For a new source or target, implementing source.Fetcher or target.Resolver should be
all it takes.
MIT — see LICENSE.