# 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/<name>/`, where Claude
Code reads them natively.

```
┌─ mcpd (Postgres) ──────────────────────────────┐
│  Skill rows (content + files{} + metadata)     │
└────────────────┬───────────────────────────────┘
                 │ HTTP, hash-pinned diff
                 ▼
┌─ ~/.claude/skills/<name>/ ─────────────────────┐
│  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 `<name>` can exist at multiple scopes simultaneously. The two
unique constraints are `(name, projectId)` and `(name, agentId)`.

## CLI

### Create

```bash
mcpctl create skill <name> \
  [--project <name> | --agent <name>] \
  --content / --content-file <path> \
  [--description "<text>"] \
  [--priority <1-10>] \
  [--semver <X.Y.Z>] \
  [--metadata-file <path>] \
  [--files-dir <path>]
```

`--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 <name>

# Edit + bump semver
mcpctl edit skill <name> --bump major|minor|patch --note "<message>"

# Edit + set explicit semver
mcpctl edit skill <name> --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 <name>

# 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 <name>` 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 `<targetDir>.mcpctl-staging-<pid>/`, then
the existing directory (if any) is renamed to
`<targetDir>.mcpctl-trash-<pid>`, 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
