Files
Muhammad Zakir Ramadhan 9b978fdbe1 Expand the documentation with measured figures and operational detail
Most of this replaces "roughly 550 characters per tool" with the actual
per-tool measurements, and fills in the parts a reader hits after the happy
path: what a specific error means, what a setting costs, what is not covered.

Measured rather than estimated:
- Per-tool byte cost, all fourteen, and the per-set totals. 7,673 B for the
  full set, averaging 548.
- Builtin skill bodies at 5,284 B against a 681 B catalogue, which is the
  argument for loading bodies on demand.
- Full system prompt 3,571 chars, core-only 2,045.

New sections:
- tools: which sets to keep and why, the jail function itself, an output-cap
  table, and the real error strings for edit_file and multi_edit.
- configuration: env var per provider preset, cost-estimate limits, what each
  --no-* flag isolates, and three settings that do more than they look like.
- agents: step caps per variant, which variant to reach for, and the fact that
  reasoning is charged as output and discarded first by compaction.
- headless: exit code 0 means "the turn completed", not "the answer was yes" —
  with the jq pattern for gating on content. Timeouts, concurrent -c runs
  fighting over one session, CI recipes for --no-skills.
- mcp: parallel connect, startup cost, a debugging ladder, and that toolSets
  does not gate MCP tools.
- registry: publishing, local testing over http://localhost, and a
  troubleshooting section keyed on the actual validator messages.
- memory: what compaction discards in what order, /compact versus automatic
  pruning, and that -c matches on cwd.
- skills: the frontmatter reader's limits, and how to verify a skill loaded.

Corrections found while cross-checking against the source:
- The guard table was missing --force-with-lease and > /dev/sd…
- The done event's token fields are optional, so the jq example filters on one
  rather than assuming it.

Two honest limits now written down: the guard matches command strings, so a
base64-decoded or script-wrapped command is not caught; and a registry index is
trusted for its contents, not its authorship.

Verified: all internal links and heading anchors resolve, every docs/ page is
reachable from the README, 538 tests pass, typecheck clean.
2026-09-03 09:26:37 +07:00

6.4 KiB

Registry

External skills and plugins, browsed and installed from the CLI.

/registry                     everything in the index
/registry search <query>      narrow by name or description
/registry installed           what is already here
/registry add <name>          fetch, show, then install on confirmation
/registry remove <name>       delete an installed entry
registry
3 of 3 available
S migration    Write or run a database migration
S commit-style Write commits the way this team does
P no-secrets   Refuses writes to credential files  installed
S skill  P plugin  |  /registry add <name>

esc dismisses the panel.

The two kinds are not equally safe

A skill is instructions. Installing one puts a stranger's words into the system prompt of every future session in this project. That is prompt injection by invitation, so the install shows the body first and /skills always says the origin:

install skill "migration"?
https://raw.githubusercontent.com/example/registry/main/skills/migration.md

  Migrations live in `db/migrations/` and are timestamped, never renumbered.

  Run `bun run db:migrate` locally first. Staging runs them on deploy.

A skill is instructions the agent follows. This text joins your system prompt.
y install | n cancel

A plugin is data. Never code. A manifest declares refusal rules; the guard that evaluates them is the same compiled code for every installed plugin:

{
  "name": "no-secrets",
  "description": "Refuses writes to credential files",
  "appendix": "The no-secrets plugin refuses writes to .env and credential files.",
  "deny": [
    {
      "tools": ["write_file", "edit_file", "multi_edit"],
      "pathPattern": "(^|/)\\.env|credentials|\\.pem$",
      "reason": "refusing to write a credential file; add secrets yourself"
    }
  ]
}

Loading TypeScript from a URL is not offered at any price. A plugin can block tool calls, so one that could also execute could read every file the agent can read and lie about blocking anything. See plugins for why this boundary exists.

What is validated before anything is written

Check Why
https only, localhost for tests file: would read a local path, data: would inline a payload
Name matches ^[a-z0-9][a-z0-9-]*$ the name becomes a filename, so ../evil must not parse
Index at most 256 KB, body at most 64 KB a hostile index should not exhaust memory
Manifest against a strict schema extra keys like beforeToolCall are dropped, not honoured
Every pattern compiles as a regex a broken pattern would fail on the first tool call instead
Pattern at most 200 characters it runs on every tool call; a pathological one is a denial of service
Body name matches the index name an index entry cannot serve something else under a trusted name
At least one deny rule a plugin with no rules is only prompt text, which is what a skill is for

A malformed installed plugin is reported by /plugins and skipped. One bad install does not stop the agent from starting.

Where installs land

~/.shiro-neko/registry/
  skills/<name>.md      loaded as origin "registry"
  plugins/<name>.json   loaded as a declarative plugin

Precedence for skills, low to high: builtin → registry → user → project. A skill you wrote in ~/.shiro-neko/skills/ or .shiro/skills/ always beats one fetched from a registry, so an install can never silently shadow your own work.

Installs take effect on the next start. A skill joins the system prompt and a plugin joins the guard chain, and both are assembled once at boot; hot-swapping either mid-session would mean a turn whose rules changed underneath it.

Pointing at your own index

{ "registryUrl": "https://example.com/my-registry/index.json" }

The index is one JSON document:

{
  "skills": [
    {
      "name": "migration",
      "description": "Write or run a database migration",
      "url": "https://example.com/skills/migration.md",
      "author": "you"
    }
  ],
  "plugins": [
    {
      "name": "no-secrets",
      "description": "Refuses writes to credential files",
      "url": "https://example.com/plugins/no-secrets.json"
    }
  ]
}

Both arrays are optional. A name may appear once as a skill and once as a plugin; /registry add skill:review disambiguates, and an ambiguous name is refused rather than guessed.

A private index is just a URL you control. There is no account, no token, and no telemetry — /registry makes exactly one GET for the index and one for the entry you install.

Publishing

Two files and a static host. GitHub raw works, and so does anything that serves JSON over https.

your-registry/
  index.json
  skills/migration.md
  plugins/no-secrets.json

Three rules the validator enforces, so worth getting right first:

  • The name in index.json must match the name inside the file. A skill's frontmatter name and a plugin manifest's name are both checked against the index entry.
  • Names are ^[a-z0-9][a-z0-9-]*$. No uppercase, no dots, no slashes.
  • A plugin needs at least one deny rule. A manifest with an appendix and no rules is prompt text, which is what a skill is for.

Test it locally before publishing. registryUrl accepts http://localhost, so:

cd your-registry && python -m http.server 8000
{ "registryUrl": "http://localhost:8000/index.json" }

/registry then exercises the real fetch, the real validation, and the real install path against your files. That is the whole loop, without pushing anything.

Troubleshooting

"the registry index is malformed: …" — the message names the first failing field. The usual causes are an uppercase name, a url that is not https, or a plugin entry with no deny.

"X calls itself Y but the index calls it X" — the file's own name disagrees with the index. Fix one of the two; the check exists so an index cannot serve something else under a name you trusted.

"invalid pattern …" — a pathPattern or commandPattern is not a valid regex. Remember it is JSON, so a backslash needs doubling: \\.env$, not \.env$.

Installed but nothing happens — installs load at startup. Restart, then check /skills or /plugins for the entry and its origin.

In /plugins with an error beside it — the manifest on disk no longer validates. It is skipped rather than fatal, so the agent still starts; /registry remove and reinstall.