# 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.
