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:
{
"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:
{
"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
{
"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.
-
denyis not seeded from the CLI's list. Those strings shipped as allow rules; the same string sitting indenyis something a human typed on purpose. Your ownretireentries 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
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
targetisproject, or the project's when it isuserGlobal. One target, one file.
Next
- Tool Surface Setup: what gets written during setup and why each write is safe
- Troubleshooting:
permissions.settings,permissions.block