Use when a user wants to persist a working REPL snippet, bash command, or multi-step workflow as a reusable cap (CAP.md tool) available in future sessions. Trigger on save_cap, cap_store, auto_active, capblob-pipe, "/build-tool", "make this reusable", or when a one-off shell pipeline is about to be retyped a third time.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add carsteneu/yesmem --skill yesmem-cap-builder --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Yesmem Cap Builder?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/carsteneu-yesmem-cap-builder)More formats (shields.io, HTML) on the badges page.
---
name: yesmem-cap-builder
description: Use when a user wants to persist a working REPL snippet, bash command, or multi-step workflow as a reusable cap (CAP.md tool) available in future sessions. Trigger on save_cap, cap_store, auto_active, capblob-pipe, "/build-tool", "make this reusable", or when a one-off shell pipeline is about to be retyped a third time.
---
# YesMem Cap Builder
A cap is a CAP.md file (YAML frontmatter + `## Purpose` + `## Scripts` + optional `## Database` + optional `## Actions`) that yesmem persists as a learnings row, deploys to `~/.claude/caps/<name>/CAP.md`, and re-injects into future sessions via the proxy. Caps are how an ad-hoc REPL command becomes a session-spanning, queryable tool.
This skill is the **discovery surface**. Side-files in this directory hold the depth — open them when you actually need them.
## Side-files (open on demand)
| File | Open when |
|------|-----------|
| `recipes.md` | You're about to build a cap and want a working template to copy |
| `api-reference.md` | You're calling `save_cap`/`get_caps`/`activate_cap`/`cap_store`/`cap_proposal_decide` and need the exact param shape |
| `gotchas.md` | A cap fails on activate, save_cap silently stores garbage, `sh()` returns truncated output, or anything else surprises you |
## External references
- **Cap-Spec (canonical format)**: `https://github.com/carsteneu/cap-spec` — treat as authoritative for CAP.md structure, frontmatter rules, and the v1.1 multi-script format. **Note**: spec.md is at v1.1-draft, but `cap-spec/examples/*/CAP.md` are still v1.0 — see `gotchas.md` "Cap-spec format drift" before copying.
- **Working caps to copy from**: `caps/bundled-caps/wiki_export/CAP.md` (current, v1.1, ~16 KB embedded base64) and `caps/bundled-caps/reddit_fetch/` are the freshest in-repo references.
- **yesmem CLI for caps**: `yesmem query --format objects` (read-only SQL across `yesmem.db`/`messages.db`/`caps.db`/`runtime.db`, projection-wrapped JSON), `yesmem json` (gojq-style filter pipeline, no `--slurpfile`), `yesmem cap-blob-put` (chunked blob ingest, bypasses the 30 KB `sh()` wall). Bash-runtime caps wire these together — see `recipes.md` "Cap that pipes `yesmem query` into `yesmem json`" and `api-reference.md` "yesmem CLI surface for caps".
## When to build a cap
- Working command/script the user will reach for again next week
- Multi-step REPL sequence likely to recur
- Output deserves to be queryable later (search, aggregate, meta-analyze)
- Trigger phrases: "save this", "build a cap", "make this reusable", "/build-tool"
## When NOT to build a cap
- One-off command for a specific URL or task today only
- Trivial bash under 20 chars
- Wrapping an existing MCP tool 1:1 with no new logic — call the MCP tool directly
- Data isn't worth persisting
When in doubt: ask the user if they'd reach for this next week. If not, don't build it.
## The CAP.md format (Cap-Spec v1.1)
```markdown
---
name: my_cap
description: One-line, written as discovery bait for future Claude.
version: 1
tags: [web, fetch]
scope: user
auto_active: false
tested: true
---
## Purpose
What it does, when to use it, expected inputs/outputs, edge cases.
## Scripts
### my_cap
kind: tool
schema: {"type":"object","properties":{"url":{"type":"string"}},"required":["url"]}
```javascript
async ({url}) => { ... }
```
## Database
```sql
CREATE TABLE cap_my_cap__hits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT,
fetched_at INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
```
**Hard rules** (parser rejects these — non-negotiable):
- `name`: `[a-z][a-z0-9_]{0,63}` — lowercase, underscores, no dashes. Must match the directory name.
- `## Scripts` is required, must contain at least one `### <name>` subsection. v1.0 single-`## Script` form is rejected.
- Each `### <name>` has exactly one code fence. Optional `kind: tool|handler`, `runtime: repl|bash`, `schema: <single-line JSON>`, `sandbox: none|standard|strict` between heading and fence (block ends at first blank line, prose, or fence).
- `kind: tool` + `runtime: bash` REQUIRES `schema:` — no derivation possible from bash. Use `schema: {"type":"object"}` for no-arg bash tools.
- `kind: handler` MUST NOT have a `schema:` field — handlers aren't called with structured args.
- `schema:` must be single-line JSON. Multi-line YAML schemas are not supported in v1.1.
- `scope: project` + `sandbox: none` → parser error. Project-scoped caps must use `standard` or `strict` (or omit).
- Cap-store table names use `cap_<capname>__<tablename>` prefix; daemon adds the prefix automatically when you call `cap_store({capability, table})`.
## save_cap real shape (Cap-Spec v1.1)
The current in-repo skill documents `handler_repl: '...', handler_bash: '...'` as separate fields. **That is the legacy shape.** The canonical v1.1 shape is `scripts:` as a stringified JSON array:
```javascript
await mcp__yesmem__save_cap({
name: 'my_cap',
description: 'One-line discovery bait',
scripts: JSON.stringify([
{ name: 'my_cap', kind: 'tool', runtime: 'repl',
body: handlerSource,
schema: JSON.stringify({type:'object', properties:{...}}) }
]),
tags: 'web,fetch',
tested: true,
test_date: '2026-05-01',
auto_active: false
});
```
`scriptsFromSaveCapParams` (`internal/daemon/handler_caps_helpers.go`) still accepts the legacy `handler_repl`/`handler_bash` fields for backward compatibility, but per-script kind/runtime overrides, multi-script bundles, and per-script sandbox profiles only work via `scripts:`. Always write new caps in the `scripts:` shape. See `api-reference.md` for return values, supersede semantics, and the inline-body-size pitfall.
## The 6 steps
### 1. Define
| Field | Notes |
|---|---|
| `name` | snake_case, unique, findable |
| `description` | one sentence, written as discovery bait — not "fetches web data" but "Fetch top N HN stories and persist into cap_hn_top__stories, queryable by score/title/date" |
| inputs | name + type + required? |
| persists data? | yes → step 2b; no → skip to 3 |
| scope | start minimal. 3-5 fields you'll actually query on. Adding fields later is cheap; un-creeping a 20-column escape-hell cap is not. |
### 2a. Handler
Pick `runtime: repl` (rich, JS in REPL VM) or `runtime: bash` (portable, callable outside REPL, scheduler-friendly). Most caps are one or the other. Multi-script bundles can mix both — see `recipes.md` "Multi-script cap".
REPL handler skeleton:
```javascript
async ({limit = 10}) => {
const raw = await sh(`curl -s --max-time 15 "${URL}"`, 20000);
return { count: 1, items: JSON.parse(raw) };
}
```
REPL VM allowlist: `sh, cat, rg, rgf, gl, put, gh, shQuote, log, str, haiku, REPO`, `await Read({...})`, `await Write({...})`, `await Edit({...})`, `await Bash({...})`, `await mcp__yesmem__*({...})`. **No** `process.env`, **no** `require`, **no** `fetch`, **no** `file()` global. See `gotchas.md` "REPL VM allowlist hallucinations".
Hard rules:
- Bash single-line; chain with `&&` or `;`. No heredoc, no multi-line.
- Timeouts always: `sh(cmd, 20000)`, `curl --max-time 15`.
- Output > 30 KB → capblob-pipe. See `recipes.md` "Cap that needs capblob-pipe".
### 2b. Database (if persisting)
```javascript
await mcp__yesmem__cap_store({
capability: 'my_cap',
action: 'create_table',
table: 'hits',
columns: JSON.stringify([
{name:'url', type:'TEXT'},
{name:'fetched_at', type:'INTEGER'}
])
});
```
`id`, `created_at`, `updated_at` are auto-added — listing them yields `duplicate column name`.
Upsert semantics: `data` with `id` → UPDATE; without `id` → INSERT (new row, new auto-id, new timestamps). Time-series capture works by omitting `id`.
Quotas: 10 tables per cap, 10 000 rows per table, 64 KB per cell. WHERE clauses are sanitized — see `gotchas.md` "sanitize_where blocked keywords".
### 3. Test
Run the handler with real inputs in the REPL before saving:
```javascript
const r = await handler({limit: 3});
r; // shape OK?
await mcp__yesmem__cap_store({capability:'my_cap', action:'query', table:'hits'});
await handler({limit: 3}); // re-run: idempotent? no duplicates?
```
Verify output shape, persisted rows, dedup on re-run, error paths (network fail, empty result, malformed input). If output approaches 30 KB, switch to capblob-pipe NOW, not later.
### 4. Schema & metadata
Input schema (single-line JSON when inlined as script metadata; can be multi-line when passed via `save_cap` since it's just a string param):
```json
{"type":"object","properties":{"limit":{"type":"integer","description":"how many"}},"additionalProperties":false}
```
`additionalProperties: false` on every object level — required by Anthropic tool-use spec.
Tags: comma-separated for `save_cap` param (`tags: "web,hn,fetch"`), array for CAP.md frontmatter (`tags: [web, hn, fetch]`).
`auto_active: true` → injected into every session automatically. Use ONLY for universally useful caps (search, generic fetchers). Start `false`; promote later.
### 4.5. Pre-save self-checks
Three checks. Skipping these is the #1 cause of caps that pass install-tests but crash on `activate_cap`.
**(a) Parse-probe.** Right after save_cap, call `mcp__yesmem__activate_cap({name})` and verify the returned `code` field is non-empty. Empty `code` = parser found no usable tool subsection = `activate_cap` will silently fail downstream.
**(b) Handler-lint against REPL VM allowlist.** Sweep your handler text:
```bash
grep -nE 'sh\(|file\(|process\.|require\(|import |globalThis|fetch\(' <handler-text>
```
| Wrong | Right |
|---|---|
| `file({action:'write', path, content})` | `put(path, content)` or `await Write({file_path, content})` |
| `process.env.X` | `(await sh('echo $X')).trim()` |
| `require('fs')`, `import x from 'y'` | use `sh`, `cat`, `put` directly |
| `JSON.parse(sh(...))` | `JSON.parse(await sh(...))` — `sh` is async |
| `globalThis.fetch`, `axios` | `sh('curl -s URL')` |
**(c) Schema cross-check.** When the cap persists data:
- Every column referenced in handler INSERT/SELECT/WHERE must appear in `## Database` (or in the `cap_store` `columns` array).
- Auto-added (`id`, `created_at`, `updated_at`) MUST NOT appear in your columns list.
- `## Database` ```sql fence accepts only `CREATE TABLE/INDEX/VIEW/TRIGGER IF NOT EXISTS`. SQL comments inside the fence are rejected. For stateless caps, leave the fence body empty or omit `## Database` entirely.
- Table names use `cap_<capname>__<tablename>` prefix in raw SQL; the cap_store API takes only the short name.
### 5. save_cap
```javascript
const handler = async ({limit = 10}) => { /* ... */ };
await mcp__yesmem__save_cap({
name: 'my_cap',
description: 'Discovery-bait one-liner',
scripts: JSON.stringify([{
name: 'my_cap',
kind: 'tool',
runtime: 'repl',
body: handler.toString(),
schema: JSON.stringify({
type:'object',
properties:{ limit:{type:'integer', description:'count, default 10'} },
additionalProperties:false
})
}]),
tags: 'web,fetch',
tested: true,
test_date: '2026-05-01',
auto_active: false
});
```
Re-saving with same name auto-supersedes the old row, bumps version. Returns `{id, name, superseded, version}`.
**Inline-body trap**: if the `scripts` JSON exceeds ~10 KB AS A LITERAL TOOL ARGUMENT, the daemon may store the literal string `"PLACEHOLDER"` instead of your body. Build the JSON in a REPL variable first, then pass via `await mcp__yesmem__save_cap({scripts})`. Never paste a 10 KB+ scripts string into a top-level tool_use call. See `gotchas.md` "save_cap PLACEHOLDER bug".
### 6. Verify
```javascript
await mcp__yesmem__get_caps({name: 'my_cap'}); // 1. row in DB
const r = await mcp__yesmem__activate_cap({name: 'my_cap'}); // 2. parse-probe
r.code; // non-empty?
eval(r.code); // 3. register in REPL
await my_cap({limit: 5}); // 4. callable
await mcp__yesmem__cap_store({capability:'my_cap', action:'query', table:'hits'});
```
After superseding a schema, existing threads must re-`activate_cap` — proxy caches per thread.
## Activation modes
| Mode | Trigger | Use for |
|---|---|---|
| `auto_active: true` | proxy injects every session | universal caps (search, general fetchers) |
| `activate_cap(name)` | per-thread MCP call | task-specific caps |
| Manual `registerTool(...)` | copy/paste returned code | ad-hoc test before save_cap |
`deactivate_cap(name)` reverses activation for the current thread.
## Bundled cap update lifecycle (CRITICAL)
Bundled caps live at `caps/bundled-caps/<name>/CAP.md`. They are also persisted as a learnings row in `caps.db`. The daemon reads from DB at runtime, and **writes DB content back to disk on restart**. So:
| Situation | Required step |
|---|---|
| Disk SHA = embedded SHA, DB = same version | edit `caps/bundled-caps/<name>/CAP.md` + `make deploy` is enough |
| DB has a HIGHER version than source | edit the source AND call `save_cap` with the new body in parallel — otherwise the daemon restart overwrites your disk edit with the older DB version |
| Source disagrees with disk after `make deploy` | DB-vs-disk write-back conflict. Diagnostic: `sha256sum caps/bundled-caps/<name>/CAP.md` vs `sha256sum ~/.claude/caps/<name>/CAP.md`; `grep '^version:' ~/.claude/caps/<name>/CAP.md`. If disk version > source version, DB wrote back. Fix via `save_cap`. |
This contradicts a previous note in the skill ("no manual save_cap needed for bundled cap update") — that was incomplete. See `gotchas.md` "DB write-back overrides disk edit".
## Sandbox
Sandbox is per-script in v1.1 (`sandbox: none|standard|strict` in script metadata). Resolution order: per-script > scheduled-job profile > daemon default. `scope: project` rejects `sandbox: none`. See cap-spec/spec.md and `internal/capfile/parse.go:120-145` for the guard.
## Auto-correct loop (handler caps run by scheduler)
When a `kind: handler` bash script fires under the scheduler with `auto_correct: true` (default) and fails, the daemon may stage a corrected version as a `cap_proposed` learning. Workflow:
```javascript
await mcp__yesmem__list_cap_proposals(); // pending
await mcp__yesmem__cap_proposal_decide({id: 12345, decision: 'accept'});
// or { decision: 'reject' }
```
Active cap is untouched until `accept`. See `api-reference.md` for the exact shapes. **Multi-script caps are now safe**: a historical bug overwrote the entire `scripts` array with a single corrected bash body — that is fixed via `script:<name>`-keyword-plumbing in `persistProposalForReview` and `handleCapProposalDecide`. Older notes saying "set `auto_correct: false` on multi-script caps" are stale. See `gotchas.md` "autoCorrectBashCap on multi-script bundles — historical bug, now resolved".
## Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| `id` / `created_at` / `updated_at` in columns array | `duplicate column name` | Remove them — auto-added |
| Bash heredoc / multiline | hook reject or shell mangle | Single-line, `&&`/`;` |
| Forgot `create_table` at handler start | first call passes, later fails on fresh DB | Always `create_table` first — idempotent |
| `tested: true` without running | bugs surface on first real use | Actually run with real inputs first |
| `auto_active: true` for rare/experimental caps | every session pays the token cost | Start `false`, promote after validation |
| No timeout on `sh`/`curl` | hung handler blocks REPL | `sh(cmd, 20000)`, `curl --max-time 15` |
| User input concatenated into WHERE | sanitizer-bypass / blocked keyword | `?` placeholders + `args` array |
| Silent error swallow | user thinks it worked | Return `{error: ..., detail: ...}`, don't throw |
| Inline Python/Perl/awk in bash with nested quoting | `r'\''...\''` escape hell | Move logic into REPL JS, or extract a helper file |
| Schema change without `activate_cap` in open threads | old threads see old shape | Tell user to re-activate after supersede |
## Quick mode (3 steps, trivial caps only)
For caps with no storage and a single bash one-liner:
1. Working one-liner in REPL
2. `save_cap({name, description, scripts: JSON.stringify([{name, kind:'tool', runtime:'bash', body, schema:'{"type":"object"}'}]), tags, tested: true})`
3. `get_caps({name})` to confirm
Most real caps need the full 6 steps.
## Related
- `recipes.md` (this directory) — six concrete patterns with complete CAP.md
- `api-reference.md` (this directory) — exact MCP shapes and return values
- `gotchas.md` (this directory) — every known pitfall with symptom/cause/fix
- `internal/storage/cap_store.go` — storage layer + sanitize_where + quota constants
- `internal/daemon/handler_caps.go` + `handler_caps_helpers.go` — save/activate/deactivate
- `internal/daemon/handler_cap_proposal.go` — auto-correct accept/reject
- `internal/proxy/caps_inject.go` — `auto_active` injection mechanics
- `internal/capfile/parse.go` — CAP.md parser; the v1.1 contract is enforced here
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!