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/
.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.json
{
  "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.

Terminal
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

Terminal
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