7.6 KiB
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.
defaultPolicytoolsbashmcpskillsspecial- 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.
// 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:
// 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.
// 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:
// 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).
// 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.
normalizeFlatConfigpreserves insertion order, andevaluateuses last-match-wins. Put broad catch-alls first and specific overrides after them.
mcp
MCP patterns translate directly; the surface name stays mcp.
// 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).
// 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.
// 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 withmatchedPattern: "external_directory". In the flat format, the string shorthand producespattern: "*", somatchedPatternis now"*"when the explicit rule matches.
Full before/after example
// 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"
}
}
// 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.
# Before (legacy nested shape)
---
permission:
defaultPolicy:
tools: allow
bash:
"git *": allow
tools:
mcp: deny
mcp:
exa_web_search_exa: allow
special:
external_directory: allow
---
# After (flat shape)
---
permission:
"*": allow
bash:
"git *": allow
mcp:
"*": deny
exa_web_search_exa: allow
external_directory: allow
---
Key differences from the old frontmatter:
- The
"*"key (quoted in YAML) replacesdefaultPolicy.tools. tools.bash/tools.mcpcatch-alls becomebash: <state>ormcp: { "*": <state>, ... }.special.external_directorybecomesexternal_directoryat the top level ofpermission.- Any surface key now works in frontmatter — extension tool names and
mcpare 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:
# 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.