Skills
The playbook shelf: reusable procedures any agent can read through MCP, and an agent host can invoke natively once they're synced to disk. This chapter covers what a skill is, the hybrid design that keeps Ledgenter the source of truth, and the sync contract that keeps the disk copies honest.
A shared library of playbooks the agents can read and add to.
Skills are reusable how-to guides — "here's how we do X" — that any agent can look up, follow, and contribute to as it learns. One agent's hard-won procedure becomes something the whole team reuses, instead of every agent rediscovering it.
Because a skill is something other agents will follow, Ledgenter handles it with care: a skill can only hold instructions in plain words, it stays inside its own area, and any change to it shows up plainly. Within your workspace, treat a shared skill with the same care you'd give code that others will run.
What a skill is
A skill is a reusable how — a procedure, a checklist, a playbook that one agent writes once and every agent thereafter follows. Ledgenter already has two other rooms for durable text, and the boundary between the three is deliberate:
| Room | Captures | Shape |
|---|---|---|
| Skill | A reusable HOW — procedures, checklists | Versioned, slug-identified, materializable to disk |
| Knowledge | Findings and facts — what was learned | Searchable notes, semantic by default |
| Decision | A choice plus its rationale | Append-only; superseded, never edited |
A skill is deliberately not a kind of knowledge note. It is a distribution
artifact with structural identity: the slug doubles as its on-disk directory name, the
one-line description becomes the host's "when to use this" trigger text, and a version
counter plus a content_hash (sha256 of the body) make drift between the
database and any synced copy detectable. Like everything else in Ledgenter, skills are tenant
data — row-level-security scoped, attributed to the actor and run that wrote them, and
reachable through the global search tool.
The hybrid design and its honest premise
The design starts from a constraint Ledgenter cannot change: agent hosts like Claude Code
load natively-invocable skills only from disk (.claude/skills/).
An MCP server cannot register one at runtime. So either the disk is the source of truth —
with no tenant isolation, no versions, no attribution, no search, and no concurrency story —
or Ledgenter is, and the disk is a materialization. Ledgenter chose the second, and is explicit
about what that buys and what it costs:
Ledgenter (source of truth: RLS, versions, attribution, search) │ ├── skill_list / skill_get ──► any agent, over MCP // NEVER stale │ └── ledgenter skills sync ──► <checkout>/.claude/skills/ledgenter--<slug>/SKILL.md // host loads natively; can lag; manifest detects drift
The staleness story is therefore honest by construction. Reads through MCP —
skill_list for the effective set, skill_get for a full body — are
never stale, because they hit the database. Only the synced disk copies can lag, and the
sync manifest's hashes make that lag detectable rather than silent. An agent that only needs
to follow a procedure can read it over MCP and never touch the disk at all; the
sync exists for the host's native invocation path.
Authoring with skill_upsert
One tool, skill_upsert, both creates and updates — keyed by the scope and
the slug. Two agents racing to create the same slug converge (an ON CONFLICT
insert; the loser falls through to the update path) instead of one of them hitting a
duplicate-key error.
skill_upsert( slug: "release-checklist", // stable identity — and the on-disk dir name description: "Use when shipping a release: pre-flight checks and the rollback drill.", body: "## Steps\n1. Run the gates…", // plain markdown — NO frontmatter fence tags: ["release", "ops"] ) // → { ok: true, slug, version: 1, status: "active", content_hash, created: true } // later, a concurrent-safe edit: skill_upsert(slug: "release-checklist", body: "…", expected_version: 1) // stale → error: "expected version 1 but skill is at version 2"
| Field | Rule | Why |
|---|---|---|
slug | Must match ^[a-z0-9][a-z0-9-]{0,63}$ | It becomes a filesystem directory name — the grammar is the first defense against path traversal |
description | Required on create, ≤ 1 KB | It is the one-line "when to use this" — the trigger text the host reads to decide whether to invoke |
body | Required on create, ≤ 64 KB, must not begin with --- | Plain markdown only; frontmatter is generated, never accepted (below) |
name | Optional; defaults to the slug | Display title; folded into the generated frontmatter |
expected_version | Optional compare-and-set | A concurrent edit becomes a hinted error, not a silent clobber |
status | active or archived | Archiving folds into the same tool — no delete-and-recreate |
The frontmatter rule deserves its why. On disk, a SKILL.md begins with a
YAML block that hosts treat as configuration — keys like allowed-tools
and model change how the host behaves. Ledgenter therefore generates that block
itself, from exactly two typed columns (name, description, both
YAML-escaped as quoted single-line strings), and rejects any body that smuggles its own
--- fence — at write time, in both the RPC and a table constraint. Tenant text
can instruct an agent; it can never configure the host.
The version counter increments only when content changes — body, name, or description. Re-tagging or archiving does not mint a new version, so a version bump always means "the procedure itself changed", which is exactly the signal sync needs.
Scoping and shadowing
Every skill is either tenant-wide (project_id null — the shared library) or
scoped to one project. When a project-scoped skill and a tenant-wide one collide on a slug,
the project-scoped skill wins: it is the one materialized and the one
skill_get resolves in that project's context. The losing tenant-wide row is not
hidden — it stays in skill_list results marked shadowed_by (the
winner's id), so a list or a sync report can explain what happened rather than leaving a
skill mysteriously absent. This gives a project a clean override mechanism: publish the
house-standard release-checklist tenant-wide, and let the one project with a
genuinely different release process shadow it under the same name.
ledgenter skills sync
ledgenter skills sync is the bridge to native invocation: it materializes the
effective set — tenant-wide plus the project's own, shadowing already resolved — into
<checkout>/.claude/skills/ledgenter--<slug>/SKILL.md. The
ledgenter-- prefix keeps Ledgenter-managed directories visually and mechanically
distinct from anything you authored by hand.
ledgenter skills sync # inside a checkout: project inferred from the linked repo ledgenter skills sync --check # report drift, write nothing, exit 1 when out of date (cron-friendly) ledgenter skills sync --force # deliberately overwrite locally-edited copies ledgenter skills push .claude/skills/ledgenter--release-checklist # upstream a local edit to Ledgenter ledgenter skills sync --global # tenant-wide skills only, into ~/.claude/skills (opt-in)
Everything that makes sync trustworthy hangs off one file: the manifest
(.ledgenter-skills-manifest.json), written beside the skills, recording each
materialized skill's id, slug, version, content_hash, path, and a
file_hash of the rendered file as written. Every write and every prune
is scoped to the manifest. The consequences:
- Hand-authored skills are never touched. An unmanifested path that collides with a Ledgenter slug is reported as a named conflict — sync refuses to adopt or overwrite it.
- Local edits are protected. If a materialized file's hash no longer
matches the manifest's
file_hash, someone edited it; sync skips it with a warning and suggestsledgenter skills pushto upstream the change.--forceoverwrites only deliberately. - Pruning is bounded. When a skill leaves the effective set — archived, deleted, or descoped — sync removes its directory, but only ever paths the manifest lists.
- Git stays clean, locally. Ignore entries for
ledgenter--*and the manifest go to.git/info/exclude— never the committed.gitignore, so one developer's sync never edits a file the whole team shares.
Project scope is inferred from the checkout itself: the cwd's git remote is fingerprinted,
matched to a registered repository, and followed to its linked project — but only when that
chain is unambiguous (exactly one linked project); otherwise sync asks for
--project explicitly. --global is opt-in and materializes
tenant-wide skills only into ~/.claude/skills — a machine may
serve multiple tenants, and the user-level directory loads into every session, so
project-scoped material never belongs there.
Keeping copies fresh. Re-run sync to pick up changes; loop agents
sync before each tick. --check in cron gives a standing drift alarm.
Operators who want sync on every session start can wire a host hook — Ledgenter documents the
pattern but never installs hooks itself.
The trust posture
Any actor with write scope can author or edit a skill — a deliberate decision, and the product point: agents capture playbooks as they learn, and gating authorship on humans would force a handoff for every learned procedure. Because a skill is content one tenant-mate writes and other agents may follow, it is handled with the same care as any tenant-authored text — with strictly more structure around it.
That structure is the containment:
- Generated frontmatter. Bodies carrying their own
---fence are rejected at write time, so a skill can never inject host configuration (allowed-tools,model, hooks) — only words. - Triple slug validation. The path grammar is checked three times: a
table
CHECKconstraint, a re-validation inside the RPC, and a client-side path-grammar check before any path join — server data is never trusted for filesystem paths, so a row can never escape the skills directory. - Manifest and hash discipline. A swapped skill surfaces as an update in the sync report, and locally-edited copies are never silently overwritten.
The tenant is the trust boundary. Within it, treat a skill other agents will execute as carefully as code you would run yourself. The structure above keeps a skill to words inside its own directory; the rest is the same cooperative-but-fallible model the whole product assumes.