Files

366 lines
7.6 KiB
Markdown

# Migration guide: legacy format → flat permission format
This guide covers migration from the pre-#66 config format to the flat `permission` format introduced in #66.
## Summary of changes
The old format had six top-level policy keys (`defaultPolicy`, `tools`, `bash`, `mcp`, `skills`, `special`).
The new format has a single `permission` key whose top-level entries map surface names to actions.
Runtime knobs (`debugLog`, `permissionReviewLog`, `yoloMode`) are **unchanged** and stay at the top level.
## Checklist
Go through each section below.
For each key present in your config, apply the translation and remove the old key.
- [ ] `defaultPolicy`
- [ ] `tools`
- [ ] `bash`
- [ ] `mcp`
- [ ] `skills`
- [ ] `special`
- [ ] Per-agent frontmatter
## Translation reference
### `defaultPolicy`
`defaultPolicy` set per-surface fallback actions.
In the flat format the universal fallback is `permission["*"]`; per-surface catch-alls are entries in the `permission` object.
```jsonc
// Before
{
"defaultPolicy": {
"tools": "ask",
"bash": "ask",
"mcp": "ask",
"skills": "ask",
"special": "ask"
}
}
// After — all surfaces default to "ask" via the universal fallback
{
"permission": {
"*": "ask"
}
}
```
If surfaces had **different** defaults, express each one explicitly:
```jsonc
// Before
{
"defaultPolicy": {
"tools": "allow",
"bash": "ask",
"mcp": "ask",
"skills": "ask",
"special": "deny"
}
}
// After
{
"permission": {
"*": "allow",
"bash": "ask",
"mcp": "ask",
"skill": "ask",
"external_directory": "deny"
}
}
```
### `tools`
Each entry in `tools` maps a tool name to a permission.
In the flat format, tool names are surface keys directly inside `permission`.
```jsonc
// Before
{
"tools": {
"read": "allow",
"write": "deny",
"edit": "ask"
}
}
// After
{
"permission": {
"read": "allow",
"write": "deny",
"edit": "ask"
}
}
```
**Special case — `tools.bash` and `tools.mcp`:** These were catch-all overrides for their respective surfaces.
In the flat format, use a string shorthand or an explicit `"*"` pattern:
```jsonc
// Before
{
"tools": { "bash": "allow", "mcp": "deny" }
}
// After — string shorthand (equivalent to { "*": "allow" })
{
"permission": {
"bash": "allow",
"mcp": "deny"
}
}
```
### `bash`
Bash patterns translate directly; the surface name stays `bash`.
If you also had a `tools.bash` or `defaultPolicy.bash` value different from `defaultPolicy.tools`, add an explicit `"*"` catch-all pattern at the **start** of the object (so specific patterns placed after it override it via last-match-wins).
```jsonc
// Before
{
"defaultPolicy": { "tools": "allow", "bash": "ask" },
"bash": {
"git status": "allow",
"git diff": "allow",
"git *": "ask",
"rm -rf *": "deny"
}
}
// After
{
"permission": {
"*": "allow",
"bash": {
"*": "ask",
"git status": "allow",
"git diff": "allow",
"git *": "ask",
"rm -rf *": "deny"
}
}
}
```
> **Note:** Pattern ordering matters within an object.
> `normalizeFlatConfig` preserves insertion order, and `evaluate` uses last-match-wins.
> Put broad catch-alls **first** and specific overrides **after** them.
### `mcp`
MCP patterns translate directly; the surface name stays `mcp`.
```jsonc
// Before
{
"mcp": {
"mcp_status": "allow",
"mcp_list": "allow",
"exa:*": "allow",
"dangerous-server": "deny"
}
}
// After
{
"permission": {
"mcp": {
"mcp_status": "allow",
"mcp_list": "allow",
"exa:*": "allow",
"dangerous-server": "deny"
}
}
}
```
### `skills`
The surface name changes from `skills` (plural) to `skill` (singular).
```jsonc
// Before
{
"skills": {
"*": "ask",
"librarian": "allow",
"dangerous-*": "deny"
}
}
// After — note: "skills" → "skill"
{
"permission": {
"skill": {
"*": "ask",
"librarian": "allow",
"dangerous-*": "deny"
}
}
}
```
### `special`
`special.external_directory` becomes a top-level surface key in `permission`.
Other deprecated keys (`doom_loop`, `tool_call_limit`) are simply dropped.
```jsonc
// Before
{
"special": {
"external_directory": "ask",
"doom_loop": "deny",
"tool_call_limit": "deny"
}
}
// After — doom_loop and tool_call_limit are removed entirely
{
"permission": {
"external_directory": "ask"
}
}
```
> **Note:** In the old format, `special.external_directory: "deny"` produced a rule with `matchedPattern: "external_directory"`.
> In the flat format, the string shorthand produces `pattern: "*"`, so `matchedPattern` is now `"*"` when the explicit rule matches.
## Full before/after example
```jsonc
// Before (legacy format)
{
"$schema": "...",
"debugLog": false,
"permissionReviewLog": true,
"yoloMode": false,
"defaultPolicy": {
"tools": "ask",
"bash": "ask",
"mcp": "ask",
"skills": "ask",
"special": "ask"
},
"tools": {
"read": "allow",
"write": "deny"
},
"bash": {
"git status": "allow",
"git *": "ask"
},
"mcp": {
"mcp_status": "allow"
},
"skills": {
"*": "ask"
},
"special": {
"external_directory": "ask"
}
}
```
```jsonc
// After (flat format)
{
"$schema": "...",
"debugLog": false,
"permissionReviewLog": true,
"yoloMode": false,
"permission": {
"*": "ask",
"read": "allow",
"write": "deny",
"bash": {
"git status": "allow",
"git *": "ask"
},
"mcp": {
"mcp_status": "allow"
},
"skill": { "*": "ask" },
"external_directory": "ask"
}
}
```
## Per-agent frontmatter
Frontmatter in agent `.md` files uses the same flat shape under the `permission` key.
```yaml
# Before (legacy nested shape)
---
permission:
defaultPolicy:
tools: allow
bash:
"git *": allow
tools:
mcp: deny
mcp:
exa_web_search_exa: allow
special:
external_directory: allow
---
```
```yaml
# After (flat shape)
---
permission:
"*": allow
bash:
"git *": allow
mcp:
"*": deny
exa_web_search_exa: allow
external_directory: allow
---
```
Key differences from the old frontmatter:
1. The `"*"` key (quoted in YAML) replaces `defaultPolicy.tools`.
2. `tools.bash` / `tools.mcp` catch-alls become `bash: <state>` or `mcp: { "*": <state>, ... }`.
3. `special.external_directory` becomes `external_directory` at the top level of `permission`.
4. Any surface key now works in frontmatter — extension tool names and `mcp` are no longer silently ignored.
## Behavioral differences
### Agent scope catch-alls override parent scope patterns
In the old format, `tools.bash: allow` (override layer) was lower priority than config-layer patterns from any scope, including global.
In the flat format, `bash: allow` in an agent scope is a config-layer catch-all with **higher** priority than global-scope patterns (last-match-wins, agent rules come later).
If you relied on global `rm -rf *: deny` surviving an agent's `tools.bash: allow`, you must now explicitly deny the pattern within the agent's own `bash` object:
```yaml
# Old agent frontmatter — global "rm -rf *": "deny" survived
permission:
tools:
bash: allow
# New agent frontmatter — must repeat the deny if you want it preserved
permission:
bash:
"*": allow
"rm -rf *": deny
```
### `matchedPattern` for `external_directory`
In the old format, an explicit `special.external_directory: "deny"` rule had `matchedPattern: "external_directory"`.
In the flat format, `external_directory: "deny"` (string shorthand) has `matchedPattern: "*"`.
Code that inspected `matchedPattern` to detect explicit external-directory config must be updated.