# Skills Skills are Claude Code skill bundles distributed by mcpctl. Each skill is a named bundle of files — at minimum a `SKILL.md` explaining the skill's purpose and triggers, optionally with auxiliary scripts, templates, or data files. The mcpctl daemon (mcpd) is the source of truth; `mcpctl skills sync` materialises the skills onto each dev machine under `~/.claude/skills//`, where Claude Code reads them natively. ``` ┌─ mcpd (Postgres) ──────────────────────────────┐ │ Skill rows (content + files{} + metadata) │ └────────────────┬───────────────────────────────┘ │ HTTP, hash-pinned diff ▼ ┌─ ~/.claude/skills// ─────────────────────┐ │ SKILL.md │ │ scripts/setup.sh │ │ … │ └────────────────────────────────────────────────┘ ``` ## Trust model Skills are added by senior admins together with a security reviewer at publish time on mcpd. Once content is in mcpd, clients trust what mcpd serves — no client-side sandboxing, no signature checks, no consent prompts. The rigor lives on the publishing side (RBAC, audit, the reviewer queue). See [proposals.md](proposals.md) for the review→approve flow. If you're publishing skills to clients you don't trust (e.g. an open- source distribution), the design is wrong for that — the skill format itself is fine, but the unguarded client trust assumption isn't. ## Scoping A skill attaches to one of: - **Global** — `projectId` and `agentId` both null. Synced onto every dev machine when its sync runs (with or without a project context). - **Project-scoped** — `projectId` set. Synced onto machines whose `.mcpctl-project` marker matches. - **Agent-scoped** — `agentId` set. Surfaced administratively via the API; not currently materialised onto disk by `mcpctl skills sync` (see "Future" below). The same `` can exist at multiple scopes simultaneously. The two unique constraints are `(name, projectId)` and `(name, agentId)`. ## CLI ### Create ```bash mcpctl create skill \ [--project | --agent ] \ --content / --content-file \ [--description ""] \ [--priority <1-10>] \ [--semver ] \ [--metadata-file ] \ [--files-dir ] ``` `--content-file` provides the `SKILL.md` body. `--metadata-file` accepts YAML or JSON; see "Metadata" below for the schema. `--files-dir` walks a directory tree into the `files{}` map (UTF-8 only; non-text files rejected — extend later if needed). ### Edit ```bash # Edit content in $EDITOR mcpctl edit skill # Edit + bump semver mcpctl edit skill --bump major|minor|patch --note "" # Edit + set explicit semver mcpctl edit skill --semver 1.2.3 ``` Each save records a `ResourceRevision` automatically. See [revisions.md](revisions.md). ### Sync to disk ```bash # In a project directory (with .mcpctl-project marker): mcpctl skills sync # Override project: mcpctl skills sync --project # Globals only (no project context, no marker): cd / && mcpctl skills sync # Used by the SessionStart hook — fail-open on network errors: mcpctl skills sync --quiet ``` Useful flags: | Flag | Purpose | |---------------------|-----------------------------------------------------------| | `--dry-run` | Print what would change, don't write anything. | | `--force` | Overwrite locally-modified skills. | | `--quiet` | Suppress output unless something changed; fail-open. | | `--keep-orphans` | Don't remove skills no longer in the server set. | | `--skip-postinstall`| Reserved for the postInstall executor (deferred). | ## Project setup `mcpctl config claude --project ` does the full pickup chain: 1. Writes `.mcp.json` so Claude Code routes MCP traffic through mcplocal. 2. Writes `.mcpctl-project` (single line, project name) so `skills sync` knows which project's skills to pull when run from anywhere under that directory. 3. Runs an initial `skills sync` synchronously. 4. Installs a SessionStart hook in `~/.claude/settings.json` that runs `mcpctl skills sync --quiet` before every Claude session. Tagged with `_mcpctl_managed: true` so subsequent runs find and update it instead of duplicating it. Pass `--skip-skills` to opt out of steps 2–4 (useful in CI). ## Metadata The `metadata` field is a typed JSON blob: ```yaml hooks: PreToolUse: - type: command command: "echo before-tool" PostToolUse: - type: command command: "echo after-tool" SessionStart: - type: command command: "echo session-started" mcpServers: - name: my-grafana fromTemplate: grafana project: monitoring postInstall: scripts/install.sh preUninstall: scripts/cleanup.sh postInstallTimeoutSec: 60 ``` **v1 sync executes none of these — they're stored verbatim and materialisation is deferred to a follow-up.** Once enabled: - `hooks` will be written into `~/.claude/settings.json` with `_mcpctl_managed: true` markers (see Project Setup above for how the SessionStart hook works today). - `mcpServers` will be auto-attached via the mcpd attach API. - `postInstall` will run as the user with a curated env, hard timeout, and an audit event emitted back to mcpd. Hash-pinned: re-syncs of unchanged scripts won't re-execute. ## State `~/.mcpctl/skills-state.json` tracks the last-synced state: - per-skill: `id`, `semver`, `contentHash` (matches mcpd's hash), `installDir`, per-file `sha256` + size, `postInstallHash`, `lastSyncedAt`. - top-level: `lastSync`, `lastSyncProject`, `schemaVersion`. The state file is written atomically (temp + rename). Per-file SHA-256 detects local edits — sync warns and skips modified files unless you pass `--force`. State lives outside `~/.claude/skills/` deliberately so Claude Code doesn't see our bookkeeping in its tree. ## Atomic install Each skill is staged under `.mcpctl-staging-/`, then the existing directory (if any) is renamed to `.mcpctl-trash-`, the staging dir is moved into place, and the trash is rmtree'd. A concurrent reader (Claude Code starting up) never sees a partial tree. Symmetric atomic delete for orphan removal: rename to trash, rmtree. Locally-modified skills are preserved (warned + skipped) unless `--force`. ## Failure semantics | Situation | Exit code | Behaviour | |----------------------------------|-----------|------------------------------------| | Network/timeout in `--quiet` | 0 | Skip silently. SessionStart hook never blocks Claude. | | Auth failure | 1 | "run mcpctl login" message. | | Disk full / state save failure | 2 | Loud error. | | Per-skill error | 0 | Logged in result errors[]; sync continues. | The fail-open behaviour in `--quiet` is non-negotiable — a hung mcpd must never block Claude Code starting up. ## Future The following are deferred to follow-up PRs: - `metadata.hooks` materialisation into `~/.claude/settings.json` - `metadata.mcpServers` auto-attach - `metadata.postInstall` execution with curated env + audit emission - Agent-scoped skills synced to disk (would need an agent-identity-on- disk concept that doesn't exist yet) - Bundle backup support for skills (bundle-backup is one path; git-backup is the other and is wired today) - `mcpctl apply -f skill.yaml` declarative skill apply