Troubleshooting

Fix the FlyDocs tool surface when it fails quietly: tools not showing up, permission prompts, a stale CLI, flydocs run problems, usage capture, release signing and Bitbucket authentication, organised by doctor check ID.

Every part of the FlyDocs tool surface fails quietly. A registration with the wrong command is a server your editor never spawns. A missing permission block is an approval prompt on every operation, usually blamed on the agent being annoying. A stale interpreter record is a bridge falling back to a bare python3, which is wrong on Windows. None of that prints an error anywhere.

So the first move is always the same:

flydocs doctor --tools

Every check has a stable ID — mcp.registration.claude, permissions.block, run.interpreter. This guide is organised by those IDs, because they are what the report gives you and what a CI log can be grepped for. --json emits the same results as one document; --fix repairs what FlyDocs owns and re-runs every check.

Symptom → check

What you see Check first
No FlyDocs tools in the editor at all mcp.registration.*, mcp.command, mcp.server
Tools listed but every call fails mcp.command, runtime.python
An approval prompt on every FlyDocs action permissions.block, permissions.settings
An approval prompt on every file edit permissions.default-mode
Your hooks stopped firing too permissions.settings
A tool call opened interactive onboarding (old CLI) mcp.command
flydocs run says it does not know an operation run.registry, run.dry-run
flydocs run cannot start anything run.interpreter
A skill's operations are missing from --list run.registry
Everything is checked against the wrong directory surface.scope
No AI usage arriving from a machine otel.pipe
flydocs update aborts with SIGNATURE_INVALID release.signing
New skills never reach your editor after flydocs update Install layout
flydocs init cannot clone your repos Bitbucket auth

Tools not showing up

The single most common cause is that the editor has not been restarted. MCP registrations are read at startup. If a flydocs update just ran, restart the editor before anything else. In Cursor, also confirm the server is toggled on under Settings → Cursor Settings → MCP, because a newly registered project server needs a one-time enable.

After that, walk the chain in this order — each check answers a different question, and fixing them out of order wastes time.

  1. mcp.registration.claude / mcp.registration.cursor / mcp.registration.vscode — does the client's registration file name flydocs mcp? (Classic VS Code reads .vscode/mcp.json only; VS Code's Agent Host reads .mcp.json, which is mcp.registration.claude.)
    • missing file or missing entry: the client has nothing to spawn. Run flydocs doctor --tools --fix (or flydocs update). Only the flydocs key is touched.
    • stale: the entry exists but points somewhere else — an older command spelling, or an absolute path from a previous install. The same fix replaces it.
    • foreign entry (a warning, not a failure): there is a flydocs entry that FlyDocs did not write. It is left alone, deliberately, because it may be yours. Remove it yourself, or re-run with the entry deleted.
    • unparseable: the file has a syntax error. FlyDocs will not overwrite it — it may hold the rest of your team's servers. Fix the JSON and re-run.

    Using Codex? Its registration is mcp.registration.codex, and it is a different animal: one machine-wide ~/.codex/config.toml, written only when you ask. Run flydocs connect codex to add it, flydocs disconnect codex to remove it. --fix will add or repair the block and never removes it, and the check stops reporting once you have declined the prompt.

    Using the remote server? flydocs connect remote registers the hosted server as flydocs-cloud, checked by mcp.remote.registration.claude, mcp.remote.registration.cursor, mcp.remote.registration.codex and mcp.remote.registration.vscode. Not registered is fine; a warning means the entry points at a different URL. Re-run flydocs connect remote to update it.

  2. mcp.command — is flydocs on PATH, and new enough to serve mcp? Your editor spawns the command by name; a registration is worthless if the command resolves to something that cannot serve it. See Stale global CLI below.
  3. mcp.server — does this build's server actually answer? The check drives the shipped server over a real transport and completes the handshake your editor performs. A failure here is not a configuration problem: re-install the CLI (npm i -g @flydocs/cli@latest), and if it persists capture flydocs mcp --verbose output and file it.
  4. surface.scope — which directory was examined? In a multi-repo workspace, registrations live in each repo and at the root; the report names the directory it checked, so a green report against the wrong directory is visible rather than misleading.

If all four pass and the editor still shows nothing, check that the client is reading project-scoped servers at all — some clients require you to approve a newly-registered project server once, in their own UI.

This never blocks anything. It reports the size of the tool listing a deferring client carries before any tool is used, against the budget. Over budget is a warning about context cost, not about function.


Permissions still prompting

If the agent asks for approval every time it calls a FlyDocs tool or runs flydocs run, the standing rules are not in effect.

  1. permissions.settings first. If .claude/settings.json does not parse, Claude Code silently ignores the whole file — permissions and hooks. That is almost always the real story when several unrelated things stop working at once. FlyDocs never overwrites this file; fix the syntax error yourself and re-run. (Comments and trailing commas parse fine here; a genuine syntax error does not.)
  2. permissions.block. A failure means the managed block is absent or out of date. The check attaches a preview: the exact lines that would move, computed by running the writer in dry-run mode without writing anything. Apply it with flydocs doctor --tools --fix, or by running flydocs update. The previous file is backed up under .claude/.flydocs-backup/ first.

Two cases that look like bugs and are not:

  • A repo installed by an older CLI reports permissions.block as failing. init writes the block itself now, so a fresh install starts clean. An install predating that gained the ownership record only on its first flydocs update — run that (or flydocs doctor --tools --fix) and the check passes. The two standing rules were already present from the template, so tools worked meanwhile; what was missing is the record that lets FlyDocs maintain them.
  • The policy is off. Then the check passes with "nothing is managed here", and any prompting is your own settings' business. See Managed permissions.

If the block is present and current and you are still prompted, confirm which file the policy targets: target: userGlobal writes ~/.claude/settings.json, not the repo's. The check reports the path it evaluated.


An approval prompt on every file edit

Different check: permissions.default-mode. The rule block covers FlyDocs' own tools; edit prompts are governed by permissions.defaultMode, which FlyDocs distributes only when a workspace asks for it by name. Read data.state:

  • not-managed — nothing is distributed, and Claude Code's own default applies. This is the ordinary, healthy state, and prompting on edits is your own settings' business.
  • pending — the workspace distributes a value that has not reached this machine yet. Run flydocs doctor --tools --fix or flydocs update.
  • conflict — permissions.defaultMode is already in your settings file and FlyDocs did not write it, so it was left exactly as it is. The check names the value. Delete that line if you want the workspace setting to apply; keeping it keeps your value. FlyDocs will never overwrite it, and --fix does not offer to.
  • unmanaged — a defaultMode someone set by hand sits in the repo's .claude/settings.json while the workspace distributes none. Project settings outrank each developer's own, so that line pins every checkout of the repo. Remove it, or move it to .claude/settings.local.json.
  • stale — the workspace distributes nothing (or turned the policy off) and this machine still carries a value FlyDocs wrote. --fix withdraws it, and names it. If you edited the value by hand since, FlyDocs withdraws only its ownership record and leaves your line alone.

Note what acceptEdits does not do: shell commands and every other tool still go through the permission allowlist. It moves one decision, not the gate — and it does not cover .claude/ itself, which Claude Code treats as a protected path and prompts on regardless. See Managed permissions.

A separate check, permissions.auto-noop, warns when any project-level settings file (.claude/settings.json or .claude/settings.local.json) sets defaultMode: "auto". Claude Code honours auto only from ~/.claude/settings.json or --permission-mode auto; in a project file it is ignored and it suppresses the user-level default, so it leaves sessions below auto rather than at it. Move that line to your own user settings. It is its own check so that reporting it never blocks --fix from repairing the mode itself.


No AI usage arriving from a machine

otel.pipe answers this one, and it has five answers because "no metrics" has five different causes. Read data.state (or the check's own line — it says which state it is in).

  • not-distributed — the workspace has not turned the OTel pipe on. This passes, because it is the ordinary state. If you expected it to be on, that is a portal setting: workspace settings → AI Spend → OTel Telemetry Pipe, which an admin turns on. It cannot be enabled until usage collection is.
  • distributed — the block is present and current, and this machine is configured to export. If nothing is arriving anyway, the problem is past this check: restart Claude Code (the exporter reads env at startup), then look at the endpoint the check names.
  • pending — the workspace has opted in and this machine has not been written yet. Run flydocs doctor --tools --fix or flydocs update. The check attaches a preview of the exact change, with the Authorization header redacted.
  • stale — the workspace has opted out and a block is still here. Same fix; until it runs, this machine keeps exporting into an endpoint that now refuses it.
  • unknown — no workspace config resolves in this directory, so there is nothing to say. This is what a multi-repo workspace root reports: the root has no .flydocs/config.json of its own, and the pipe is managed per repo. Run flydocs doctor inside a child repo to see its state. Nothing is written or retired from a root, deliberately — the alternative destroys the block the child repos just wrote.
  • blocked — the pipe is on and something stopped the write. In every case nothing on disk is touched: a run that cannot write the block correctly is not allowed to remove one an earlier run wrote.
    • no-credential: there is no FlyDocs API key on this machine. Run flydocs login (or set FLYDOCS_API_KEY; on a headless machine login uses a device code, or pass --device). The block carries an Authorization header built from this machine's own key, so FlyDocs writes nothing rather than a placeholder — an unsubstituted block would be refused on every request, silently, forever. An existing block keeps working meanwhile.
    • user-env-conflict: you already set one of the variables the block wants — your own OTEL_EXPORTER_OTLP_ENDPOINT, typically. FlyDocs will not take over a variable it did not create, and will not write the rest either: that would leave your endpoint in place while our Authorization header arrived, pointing our credential at your collector. The colliding names are in the check's detail. Rename or remove them, or turn the pipe off.
    • foreign-block: the settings file holds a block belonging to a different workspace. Two workspaces cannot both manage one file — switch one of them to the project target in workspace settings. Neither block is touched.
    • unparseable: the settings file does not parse. Fix it and re-run; permissions.settings is reporting the same thing.
    • placeholder-remains: a server-side bug — the block's declared placeholder does not match its own template. Report it.

Note that the pipe is only ever a supplement. Sessions captured by the FlyDocs CLI are unaffected by every state above; this pipe exists for machines that have no CLI on them.


Stale global CLI

Symptom: tools appear and then fail, or mcp is reported as an unknown command. On a CLI old enough to predate the router fix, an unknown command fell through to interactive onboarding instead — so a tool call could hang waiting on a prompt with no terminal to answer it.

Cause: the flydocs your editor spawns is not the flydocs you think it is. The registration deliberately says flydocs, not an absolute path — so it follows PATH, the same way every other invocation does — and neither init nor update probes PATH to check what that resolves to.

mcp.command is the one place that question is asked:

  • flydocs is not on PATH (warning): install it globally, npm i -g @flydocs/cli, so the registered command resolves at all.
  • PATH resolves a version older than the one that introduced flydocs mcp (failure): update the global install — flydocs self-update, or npm i -g @flydocs/cli@latest. The server command first shipped in 1.3.0, so 1.3.0 is the minimum. Until then every registration on this machine spawns a CLI without the server command.
  • PATH resolves a different version than the build being diagnosed (warning): usually a linked development build alongside a global install. Your editor will spawn the PATH copy, not the one you just ran.

Note that mcp.command reads doctor's PATH. An editor spawns with its own environment, which on macOS in particular can differ from your shell's — if doctor is happy and the editor is not, compare the two.


flydocs run problems

  • run.registry — the operation registry and the bundled entrypoints load. A failure means the workflow scripts are missing or unreadable: run flydocs update to restore them. A warning means a skill's operations.json was rejected; the message names the first problem. The affected operations are not registered until the manifest validates — see Contributing operations from a skill.
  • run.interpreter — no Python 3 interpreter on PATH. Install Python 3.10 or newer, or set FLYDOCS_PYTHON to the interpreter to use.
  • run.dry-run — an operation is resolved end to end, executing nothing. A failure here with the previous two passing means resolution itself is broken rather than any single operation; flydocs run --list shows what the registry can see.
  • runtime.python — the interpreter recorded in .flydocs/config.json. Gone is a failure: every bridge and MCP tool call spawns that command. Unrecorded or drifted is a warning — the record is a cache, not the answer. flydocs doctor --tools --fix re-probes and rewrites it. The runtime key is CLI-local: excluded from the config hash, never sent to the server.

An "unknown operation" from flydocs run with a green run.registry is usually a typo or a renamed ID — the refusal prints the accepted shape, and flydocs run --list prints every ID grouped by domain.


Where your artifacts came from

release.signing reports the trust channel of the managed tree on disk — hooks, skill scripts, and the settings file that registers both. It appears only once a release has been activated in this directory; there is nothing to say about signing on a machine with no releases.

The four states, and what each one means:

  • verified — the release was signed by a key this CLI pins, and the signature checked out over the manifest we rebuilt locally. Nothing to do.
  • unsigned accepted — the release arrived with no signature. Expected while release signing is being rolled out; it becomes a warning once your CLI ships a real signing key, because at that point an unsigned release is the channel going backwards. Run flydocs update and report it if it persists.
  • trust not established — the release is signed, but this build pins a placeholder for that key rather than the key itself, so the signature was recorded and not checked. Resolved by upgrading to a CLI that ships the key.
  • verification failed — the installed tree was signed by a key this CLI does not pin at all. Run flydocs self-update: this build is older than the signing key and cannot verify the tree it is running on.

A sync that cannot verify a release never reaches this check, because it never installs anything: flydocs update aborts with SIGNATURE_INVALID, downloads nothing, and leaves the installed tree byte-identical.


Bitbucket authentication

flydocs doctor has nothing to say about this one: flydocs init clones with your git credentials, and git's own error is the whole diagnosis. Since Atlassian retired app passwords on 2026-06-09, a Bitbucket workspace is where this shows up most.

The signatures, and what each means:

  • Authentication failed for 'https://bitbucket.org/…' — no credential is stored for the host, or the stored one is a retired app password. Nothing is wrong with the repo.
  • could not read Username … terminal prompts disabled — git ran without a terminal to prompt on and found nothing in the credential helper. Seeding the helper once, interactively, fixes it.
  • You may not have access to this repository … make sure you are authenticated — Bitbucket's own phrasing. It covers both a bad token and a token missing the repository scopes, so treat it as "check the token" rather than "check your access".
  • Permission denied (publickey) — an SSH clone URL with no usable key. Add the key to your account, or load it: ssh-add ~/.ssh/id_ed25519.

Two causes account for nearly all of these: an app password that stopped working, and the Atlassian account email used as the git username. The email authenticates against Bitbucket's REST API and is rejected by git — use x-bitbucket-api-token-auth or your Bitbucket username instead.

flydocs init prints this same guidance when its pre-clone access check fails, tailored to the host. The full recipe — creating a scoped token, the scopes it needs, storing it, and the SSH alternative — is Connecting Bitbucket.

Note that the org-level Bitbucket connection in the FlyDocs portal is not involved. It serves REST API features and never authenticates git on your machine, so a healthy connection in the portal tells you nothing about why a clone failed.


Install layout

Symptom: flydocs update succeeds, but the repo you work in keeps the skills and agents it was first installed with. The release went into the folder above it.

Cause: an older flydocs init --single wrote .flydocs-workspace.json into the parent folder, which turned the parent into a one-repo workspace root. Every later update wrote there. The install layout check in plain flydocs doctor (not --tools) finds it. It flags the parent as an accidental wrapper when all three hold:

  • the workspace index lists exactly one repo;
  • that repo still has FlyDocs’ own flat tree (.claude/skills/flydocs-workflow, or the FlyDocs hooks session-start.py or stop-gate.py);
  • that repo’s AGENTS.md has no <!-- flydocs:workspace:start --> block.

A workspace you made on purpose around one clone with flydocs init --workspace looks different: the repo has its managed tree removed and carries that workspace block. Doctor reports it as looking intentional and offers no conversion. flydocs update also warns when it runs against an accidental wrapper.

To convert it back to a flat install, run this in the repo:

flydocs doctor --fix
  • It asks first. It prints the plan, then waits for a yes. With no terminal to ask in, it converts nothing and tells you to re-run with --fix --yes.
  • It keeps a backup. The entries FlyDocs writes at a workspace root (.flydocs-workspace.json, .flydocs/, .claude/, .cursor/, AGENTS.md and the like, whole directories included) move, not delete, into .flydocs-unwrap-backup-<timestamp>/ beside your repo. The plan lists each one. Anything else in the parent stays where it is.
  • It rolls back on failure. If a move fails, everything already moved is put back. If the re-sync of the repo fails after the move, the parent stays unwrapped and doctor tells you to run flydocs update --here --force in the repo.
  • It leaves your repo’s git state alone. The re-sync puts back any tracked file it changed, and the git index is never touched.

If the parent held a credential, the repo has none of its own, and the repo ignores .flydocs/credentials.json, it is copied into the repo with mode 600. Otherwise doctor says why it was not copied, and flydocs login in the repo covers it.


What --fix will and will not do

flydocs doctor --tools --fix re-runs the shipped writers — the registration reconciler, the permission writer, the interpreter probe — never a bespoke repair path. Each is idempotent, and every check runs again afterwards, so a repair that did not work still reports as a failure and a second --fix finds nothing to do.

It never touches:

  • a flydocs MCP entry that is not ours (it may be deliberate);
  • a registration file that does not parse (it may hold other servers);
  • a .claude/settings.json that does not parse (it is the only copy of whatever you were editing);
  • anything outside the flydocs key and the managed permission block.

A registration fix is scoped to the client that reported the problem, so repairing Cursor cannot rewrite a Claude Code entry a human wrote.


Reporting a problem

Include the output of:

flydocs doctor --json

It carries the CLI version, the directory examined, and every check with its status, detail, remediation and structured findings. A section that did not run is null rather than empty, so a skipped section cannot be misread as a clean one. Send it to support@flydocs.ai.


Next