Skill Operations
A skill that ships a script can register it with flydocs run — a stable operation ID, no file path, and no second permission prompt.
A skill that ships a script can register it with flydocs run,
so it is reached the same way every built-in operation is: a stable ID, no
file path, and no second permission prompt. This guide is for skill
authors — what you can contribute, the manifest schema, what your script
inherits, and how to check the result.
What Is Extensible, and What Is Not
| Surface | Extensible by a skill | How |
|---|---|---|
MCP tools (flydocs mcp) | No | Fixed product surface — see below |
flydocs run <operation-id> | Yes | operations.json beside your SKILL.md |
The MCP roster is not skill-declared, and that is a decision rather than a
gap. Every tool costs idle context in a client that does not defer tool
listings, and carries a wire name that can never change once a client has
cached it. The runner has neither problem: it costs
zero idle context — an agent learns IDs from
flydocs run --list when it needs them — and the standing
permission rule Bash(flydocs run:*) already covers whatever
you add, with no settings rewrite.
Where the Manifest Goes
.claude/skills/
├── flydocs-workflow/
│ └── scripts/ ← the bundled dispatchers
└── your-skill/
├── SKILL.md
├── operations.json ← this file
└── scripts/
└── export-tokens.py
Manifests are discovered in the .claude/skills directory that
owns the workflow scripts. In a multi-repo workspace that is the
workspace root's skills tree, not a child repo's. A skill
without an operations.json is skipped in silence — that is the
normal case.
The Manifest
{
"operations": [
{
"id": "design.tokens",
"script": "scripts/export-tokens.py",
"command": ["export"],
"summary": "Export design tokens",
"positionals": [{ "name": "target", "required": true }],
"flags": [
{ "name": "format", "kind": "value", "required": false },
{ "name": "watch", "kind": "boolean" }
],
"timeoutMs": 60000
}
]
}
That declares
flydocs run design.tokens <target> [--format <v>] [--watch].
| Field | Required | Meaning |
|---|---|---|
id | Yes | domain.verb, lowercase; the public contract |
script | Yes | Entrypoint, relative to your skill directory |
summary | Yes | One line, shown by flydocs run --list |
command | No | Subcommand chain passed before the arguments, for multi-command scripts |
positionals | No | Ordered { name, required } entries |
flags | No | See the table below |
timeoutMs | No | Override the default wall-clock budget for an operation that runs long |
Flag kinds
kind Caller types Child receives Use for value --format json --format json A single value; a second is a usage error boolean --watch --watch A switch repeatable --check 1 --check 2 --check 1 --check 2 argparse action="append" list --issues A --issues B --issues A B argparse nargs="*"
The three multi-value shapes are not interchangeable — argparse is not
forgiving about them. Declare the one your script actually parses. Flags
default to appearing after the subcommand chain; add
"position": "pre" for a flag your script declares on its
top-level parser, which argparse requires to come first.
What Is Enforced, Not Trusted
A manifest is validated at load. Every problem is reported as a warning on
stderr and the operation is dropped — never silently, because a skill
whose operations vanished without comment is indistinguishable from one
that was never installed.
- The script must resolve inside your skill directory. An
absolute path or a
../ escape is refused. A contributed
manifest is not a way to execute /usr/bin/anything.
- The script must exist at that path.
- The ID must match
domain.verb — lowercase, dot-separated. - The ID may not shadow a bundled operation, and may not
be claimed by a skill loaded earlier. Two skills claiming the same ID is
a conflict, not a merge; skills are loaded in name order.
-
--repo and --help are reserved.
An operation may not declare either, so --repo means the
same thing in every position of every invocation.
- A summary is required. An operation nobody can find in
--list is not reachable in practice.
What Your Script Inherits
Once registered, your operation is spawned exactly the way a bundled one
is.
- The repo is resolved for you. The child's working
directory is the repo that owns
.flydocs/config.json, found
from the caller's directory or from --repo <name|path>.
Your script never needs a cd in front of it.
- Arguments are an argv array. There is no shell. Quoting,
$(…), && and ; have no
meaning at any point in the path, so a value containing them is a value.
- Undeclared arguments never reach you. They stop the
invocation with a usage exit code and a message naming what was
accepted.
- The environment is an allowlist, not the caller's
shell: process basics, locale, proxy and CA-bundle variables, and every
FLYDOCS_* variable. PYTHONPATH,
PYTHONHOME, PYTHONSTARTUP and
PYTHONEXECUTABLE are blocked outright. The runner also sets
PYTHONUNBUFFERED=1, PYTHONIOENCODING=utf-8,
FLYDOCS_RUN=1 and
FLYDOCS_RUN_OPERATION=<your-id>.
- Output streams as it arrives and your exit code is
propagated unchanged. A run killed by the timeout or the output cap says
so on stderr and exits with its own distinct code.
- Interrupts reach you. SIGINT and SIGTERM are forwarded, not swallowed.
- Ops logging records the operation ID, exit code and duration
— never your arguments, and never your output.
Long Inputs: the Scratch Convention
A command line is the wrong place for a multi-line body — a rewritten issue
description, a filled template, a generated report. So the convention
across the whole runner surface is: write the body to a file under
.flydocs/scratch/, pass the path, delete the file.
mkdir -p .flydocs/scratch
cat > .flydocs/scratch/tokens.md <<'EOF'
## Palette
Multi-line content, quotes and all — this is a file, not an argument.
EOF
flydocs run design.tokens web --file .flydocs/scratch/tokens.md
rm .flydocs/scratch/tokens.md
The heredoc writes the file; the operation argument is a path,
which has no quoting problem to have. --file is the
catalog-wide spelling — declare it as a value flag if your
operation takes a body. .flydocs/scratch/ rather than a system
temp directory because it sits beside the config the operation runs
against, it is in the managed gitignore list, and when something goes wrong
the exact bytes you sent are still on disk and readable.
Checking Your Work
flydocs run --list # your IDs appear, tagged [skill:your-skill]
flydocs run --dry-run design.tokens web # resolves the entrypoint and argv, runs nothing
flydocs doctor --tools # run.registry warns about a bad manifest
--list groups by domain and marks contributed operations with
their source skill. --dry-run goes before the ID and prints
the resolved command without executing it — the fastest way to confirm
your command, position and flag kinds produce the
argv your script expects. run.registry is the doctor check
that reports a manifest the loader rejected.
There is no per-operation --help: an invocation the registry
cannot accept is refused with the accepted shape printed.
Next
- Tool Surface Setup: the surfaces your operation joins
- Troubleshooting:
run.registry, run.interpreter, run.dry-run - Skills: the skill architecture itself