Managed Permissions

FlyDocs keeps a small, owned region of .claude/settings.json in step with the tool surface — the standing rules, plus whatever your organisation adds on top.

FlyDocs keeps a small, owned region of .claude/settings.json in step with the tool surface: the standing rules that let an agent call FlyDocs tools without an approval prompt, plus whatever your organisation adds on top. This guide is for whoever sets that policy — what the modes do, what each field controls, and what FlyDocs claims ownership of.

For what a developer sees during setup, see Tool Surface Setup.

What Is Managed

Two rules, regardless of how many skills, scripts or repos exist:

.claude/settings.json
{
  "permissions": {
    "allow": ["mcp__flydocs__*", "Bash(flydocs run:*)"]
  }
}

That is the entire default surface. It is two rules rather than dozens because both sides of the tool surface are addressed by identifier — an MCP tool name and an operation ID — instead of by file path. A rule that names a path has to change whenever the layout does; these do not.

Everything else in the file belongs to you, and FlyDocs leaves it byte for byte as it found it.


The Ownership Record

Alongside permissions, FlyDocs maintains a top-level key:

.claude/settings.json
{
  "flydocsManaged": {
    "version": 2,
    "mode": "append",
    "allow": ["mcp__flydocs__*", "Bash(flydocs run:*)"],
    "deny": []
  }
}

This record is the boundary of the managed region — it records exactly which rules FlyDocs wrote, so they can be maintained and removed later without touching anything of yours. It is plain data rather than a comment marker because Claude Code rejects a settings file containing // comments, and then silently ignores the whole file — hooks included. The record deliberately carries no timestamp, so a sync that changes nothing produces no diff.


Modes

Set through the portal; delivered to each repo in .flydocs/config.json under permissionPolicy.

Mode Writes the two standing rules Writes extraAllow / deny Retires rules
off No No No
append Yes No Yes
managed Yes Yes Yes

append is the default. An unrecognised mode value falls back to append rather than to off — the standing rules are exactly what stops the agent asking for approval on every FlyDocs operation, and silently not writing them degrades into permission churn that nobody traces back to a typo in a config field.

off writes nothing and removes nothing. It is a full opt-out, not a "remove FlyDocs' rules" switch — an org that turns the policy off keeps whatever is already in its settings files.


Policy Fields

.flydocs/config.json
{
  "permissionPolicy": {
    "mode": "managed",
    "target": "project",
    "extraAllow": ["Bash(pnpm test:*)"],
    "deny": ["Bash(git push --force:*)"],
    "retire": ["Bash(python3 tools/legacy.py:*)"]
  }
}
Field Type Effect
mode off | append | managed See above
target project | userGlobal <repo>/.claude/settings.json, or ~/.claude/settings.json
extraAllow string[] Org allow rules, appended to the two standing ones (managed only)
deny string[] Org deny rules, written to permissions.deny (managed only)
retire string[] Rules removed from the file, whoever wrote them

Entries that are not usable rule strings — non-strings, empty strings, anything containing a newline — are dropped, with a warning naming the field and the count. Duplicates are collapsed.

target: userGlobal puts the block in the user's home settings instead of the repo's — the right choice for an org that wants one grant per machine rather than one per checkout. It applies to the whole block; there is no per-rule targeting.

retire and what it may remove

retire is the escape hatch for rules that are no longer wanted — including rules FlyDocs itself once shipped. The CLI seeds it with the path-shaped rules older templates carried; your list is merged with that seed. Three properties keep it safe:

  • Exact string match only. A rule is removed if it is character-for-character one of the retired strings. A similar rule you wrote is untouched.
  • deny is not seeded from the CLI's list. Those strings shipped as allow rules; the same string sitting in deny is something a human typed on purpose. Your own retire entries do apply to both lists.
  • Retiring a permission is not removing a capability. Withdrawing an old script rule does not stop the dispatcher scripts working; it withdraws a standing grant that Bash(flydocs run:*) now covers.

When the Block Is Written

Command Writes the block
flydocs init No — a fresh install gets the rules from the template
flydocs update Yes, on both the cloud and local tiers
flydocs doctor --tools --fix Yes, by running the same writer

The write is idempotent by construction: the plan is computed from the file's own bytes, so a run that changes nothing writes nothing — no backup, no rename, no mtime churn for a file watcher to react to. Before any real write, the previous file is copied to .claude/.flydocs-backup/ under a timestamped name.


Inspecting and Verifying

Terminal
flydocs doctor --tools          # permissions.settings + permissions.block
flydocs doctor --json           # both, as structured data, for CI
flydocs doctor --tools --fix    # write the block, then re-check

permissions.block fails when the block is absent or out of date, and attaches a preview — the writer's own dry run, showing the exact lines that would move. Nothing is written to produce it.

permissions.settings fails on its own when the file does not parse. That failure comes first and is not repairable by FlyDocs: a settings file with a syntax error is already disabling your hooks and permissions, and it is the only copy of whatever was being edited.


What Is Never Touched

  • Any rule FlyDocs did not write and has not been asked to retire.
  • Any other key in the settings file — hooks, environment, model settings.
  • A settings file that does not parse.
  • The user's file when target is project, or the project's when it is userGlobal. One target, one file.

Next