Skip to main content

Custom skills (Claude-format)

Tenant-private skill packages that auto-mount into every run_claude_cli call for your org. Upload a .zip, the skill is immediately available; no per-agent configuration needed.

This page covers the end-to-end flow: upload, lifecycle, verification, and troubleshooting.


What is a custom skill?

A Claude-format skill is a folder containing:

  • SKILL.md at the root, with YAML frontmatter carrying name: and description:.
  • Optionally a resources/ subfolder with supporting files (python helpers, brand assets, reference markdown, etc.).

Example layout:

brand-deck/
├── SKILL.md
└── resources/
├── brand.py
├── brand-reference.md
└── logo-light.png

When uploaded to your tenant, the entire folder is mounted at /workspace/.claude/skills/<folder-name>/ inside the sandbox container that runs run_claude_cli. Claude CLI auto-discovers skills from that path on every invocation, reads each SKILL.md's description:, and decides whether to apply it based on what the user asks.

The classic use case: a brand pack. The deck-generation skill carries python-pptx helpers and brand colours; every time an agent runs run_claude_cli to make a deck, the skill mounts automatically and the output comes back branded. No prompt engineering, no per-agent wiring.


Upload a skill

In the Custom tab of /skills:

  1. Click Upload Claude Skill (.zip) (or drag the .zip onto the drop zone).
  2. The file is validated: single top-level folder, SKILL.md present with name: and description: frontmatter, total size ≤ 10 MB, no path traversal, no symlinks.
  3. The skill row appears in the catalog with visibility=private, auto_apply=true. From this moment on, every run_claude_cli call for your tenant mounts it.

Re-uploading the same skill (matched by folder name) bumps the version and replaces the bytes. The skill id stays stable so any per-skill state (auto-apply toggle, etc.) survives.


Lifecycle

ActionWhat it doesMounted in run_claude_cli?
UploadCreates row + VFS bytes, visibility='private', auto_apply=true✓ immediately
ApproveFlips visibility to 'approved' (publish step)✓ still mounted for your tenant
Toggle auto_apply offRow stays, mount stops unless explicitly scoped per-call
DeprecateMarks skill deprecated✗ stops mounting
RevokeMarks skill revoked✗ stops mounting
DeleteRemoves row + VFS bytes (cascades marketplace listing + installs if published)✗ gone

Approve is the marketplace publishing step. It doesn't gate your own tenant's use — your skill is mounted from the moment of upload. Approving makes the skill installable by other tenants if you've also created a marketplace listing.


Storage

Skill bytes land in your tenant's VFS at /_skills/<skillId>-<version>.zip. This is the same storage layer your file-space uses, so:

  • One backup story, one audit trail.
  • The directory is system-managed (policy.system=true) and file-space hides it by default.
  • Deleting a skill via the Custom tab also removes the VFS file.

There is no .dev-skills-blob side folder any more. Every skill lives in VFS.


Verify the skill is mounted

Two options, from fastest to most thorough.

Quick check — Verify mount button

/skills → Custom tab → Verify mount. Hits GET /agent-runtime/skills/verify-mount, which calls the same SkillSandboxLoader code path the runtime uses and returns the exact file list that would be injected. Instant, no sandbox call. The result panel shows per-skill: folder, version, source (private / installed), file count, total bytes, and the full list of mounted paths under /workspace/.claude/skills/.

This is byte-for-byte trustworthy — the response IS the loader's output, not an LLM's summary.

Full end-to-end — Skill Report preset

In any agent's Test Console that has run_claude_cli enabled, pick the preset:

  • Sandbox validation → Skill Report — list mounted /workspace/.claude/skills

This actually invokes run_claude_cli in a sandbox container, writes a bash script to /workspace/skill-report.sh, executes it, and pastes the literal output. You get the real audit footer (— Claude CLI run: execution=<uuid> logs=/file-space?dirId=<uuid> backend=local-docker) tied to a VFS log dir.

Per-skill output:

## brand-deck
- Path: /workspace/.claude/skills/brand-deck
- Name: Brand Deck
- Description: Apply your organisation's brand system...
- Files: 12 totalling 466043 B
- First 6 lines of SKILL.md (verbatim): ...
- Top 10 file paths (alphabetical): ...

Closing line: REPORT COMPLETE — N skill(s) mounted, F file(s), B B total.

If the reply doesn't end with the audit footer, the agent likely confabulated and didn't actually run the tool. Clear the chat and re-run.


Auto-apply, per-skill scoping

By default every imported skill has auto_apply=true, so it mounts on every run_claude_cli call. To turn that off:

  • Toggle on the skill row — there isn't currently a per-row toggle on the consolidated table; that surface was deliberately removed. Use the API: PATCH /skills/<id>/auto-apply with { "enabled": false }.
  • Per-call override — handlers can pass skills: 'none' or skills: ['<skillId>'] in SandboxExecutionConfig to bypass or scope. The default for run_claude_cli is 'all'.

Tools that mount custom skills

Today: only run_claude_cli. The list is one constant in apps/api/src/modules/agent-runtime/skill-supporting-tools.ts — add a tool name to it when a new sandboxed CLI begins honouring the Claude-format .claude/skills/<folder>/SKILL.md convention (e.g. an eventual Codex adapter).

The Applies to column on every skill row in the Custom tab shows which tools will mount it. The Linked custom skills line on the System Skills tab is the reverse view — each tool card lists the custom skills it would receive.


Troubleshooting

The Custom tab shows my skill but run_claude_cli doesn't seem to use it.

Three checks in order:

  1. Click Verify mount — confirm the loader sees it. If not, check auto_apply is on and status isn't revoked/deprecated.
  2. Run the Skill Report preset and look at the actual audit footer — confirms the sandbox saw it.
  3. Check the skill's description: frontmatter is specific. Claude CLI matches skills against the user's prompt by description. A vague description means the CLI won't apply the skill even though it's mounted.

The reply to the Skill Report preset has no audit footer.

The agent likely didn't actually call run_claude_cli — it confabulated a plausible response from context. Clear the chat and re-run. If it keeps happening, the agent's system prompt may be steering it toward llm_cli instead; prepend "Call run_claude_cli now, not llm_cli." to the preset.

Delete is refused with "skill is published to marketplace".

The skill is in an active marketplace_listings row. The Delete button now cascades automatically: it deprecates the listing, flips active installs to uninstalled, then deletes the row + bytes. If you're still seeing the refusal, restart the api so the cascade logic ships.

My skill imported before the VFS refactor and the loader silently skips it.

The old blob_path value was a literal disk path that doesn't resolve to a VFS file id any more. Delete the skill from the Custom tab, then re-upload the .zip — it lands in VFS this time. The orphan bytes in .dev-skills-blob can be removed with Remove-Item -Recurse -Force .dev-skills-blob.


  • Architecture: docs/architecture/skill-mounting.md (in the repository) — the four-gate loader, storage layer, schema.
  • Sandbox execution: docs/architecture/sandbox-execution.md (in the repository) — how run_claude_cli containers are launched.
  • Sandbox test prompts: docs/development/sandbox-test-prompts.md (in the repository) — preset prompt design.