| 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)
---
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
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:
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)
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:
const r = await handler({limit: 3});
r;
await mcp__yesmem__cap_store({capability:'my_cap', action:'query', table:'hits'});
await handler({limit: 3});
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):
{"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:
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
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
await mcp__yesmem__get_caps({name: 'my_cap'});
const r = await mcp__yesmem__activate_cap({name: 'my_cap'});
r.code;
eval(r.code);
await my_cap({limit: 5});
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:
await mcp__yesmem__list_cap_proposals();
await mcp__yesmem__cap_proposal_decide({id: 12345, decision: 'accept'});
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:
- Working one-liner in REPL
save_cap({name, description, scripts: JSON.stringify([{name, kind:'tool', runtime:'bash', body, schema:'{"type":"object"}'}]), tags, tested: true})
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