Admission and the block/allow list

How DefenseClaw decides whether a skill, MCP server or plugin may be installed and run. The admission block sets what each scan finding does, and asset_policy holds the only block and allow lists. Both live in config.yaml.

An admin config may contain at most 4 MiB of source text and 65,536 parsed YAML nodes, including repeated asset_policy entries. Split or simplify large lists before applying them.

Admission is the check DefenseClaw runs when a skill, an MCP server or a plugin appears on a machine, or when you install one with the CLI. It answers one question: may this asset be installed and run? Two sections of config.yaml (config_version: 9) answer it, and nothing else does:

  • admission: says what a scan finding does. For each asset type it maps a severity to an action: block it, quarantine it, warn, or allow it.
  • asset_policy: holds the block list and the allow list. A name you deny is blocked without a scan. A name you allow skips the scan.

This is one case of the rule that config.yaml is the only admin intent (see Configuration: one source of truth). The older homes of this policy are read once, by the version 9 migration, and are then ignored (Secure Client devices keep the version 8 behavior): policies/rego/data.json, the skill_actions, mcp_actions and plugin_actions keys, and the operator rows of the audit database's actions table. Upgrading from config_version 8 says exactly what moves. Quarantine, disable and the scan-verdict blocks are runtime state, never policy.

How a verdict is reached

checks
denied rule
allowed rule
no rule
matches
no match
decides
A skill, MCP serveror plugin appears
On a list?asset_policy rules
Blockeddenied rule matched
Allowedscan skipped
First party?admission list
Scan, then applythe severity action
Clean, warning,allowed or rejected

List view for small screens. Use the expand button to open the drawing.

  1. A skill, MCP serveror plugin appears
    • checksOn a list?
  2. On a list?asset_policy rules
    • denied ruleBlocked
    • allowed ruleAllowed
    • no ruleFirst party?
  3. First party?admission list
    • matchesAllowed
    • no matchScan, then apply
  4. Blockeddenied rule matched
  5. Scan, then applythe severity action
    • decidesClean, warning,
  6. Allowedscan skipped
  7. Clean, warning,allowed or rejected
The admission check for one asset. The two lists are asset_policy rules and are checked first: a denied rule wins over an allowed one at the same scope. The first-party list belongs to admission and skips the scan only while allow_list_bypass_scan is true. With no scan result yet, scan_on_install decides between a scan and an immediate allow.

The check ends in one of six verdicts:

VerdictWhenWhat happens
blockedA denied rule in asset_policy matches.The install is refused before any scan. The install watcher quarantines the files of a skill or plugin that appeared anyway. Tool calls to a blocked MCP server are refused.
allowedAn allowed rule matches; or a first-party entry matches while allow_list_bypass_scan is true; or there is no scan result and scan_on_install is false; or the finding's action is allow.Nothing is enforced.
scanNothing decided yet and scan_on_install is true.The asset is scanned, then checked again with the scan result.
cleanThe scan found nothing.Nothing is enforced.
warningThe scan found something and the action is warn.The finding is recorded as an install warning. The asset stays.
rejectedThe scan found something and the action blocks, or the scanner failed.The action's install, file and runtime parts apply. A scanner that errors or exits non-zero is rejected, with install block, file quarantine and runtime disable: admission fails closed.

Precedence, first match wins:

  1. asset_policy denied: blocked. It beats every allow, including the first-party list.
  2. asset_policy allowed: allowed, no scan.
  3. admission first_party_allow_list, while allow_list_bypass_scan is true: allowed, no scan.
  4. No scan result and scan_on_install: false: allowed. With scan_on_install: true: scan.
  5. The scan result: the action for the worst severity, or the scanner-specific action when scanner_overrides has one.

The MCP scanner does not connect to a server on a loopback, private, link-local or multicast address, so the install scan of such a server fails and the verdict is rejected (fail-closed). An allowed rule with the name and url of the server (step 2) or scan_on_install: false for mcp (step 4) admits one you trust; see Servers on local and internal addresses.

Where it runs: the gateway's install watcher checks a skill, plugin or MCP server when it appears in a watched directory, and the defenseclaw install and scan commands run the same check. gateway.watcher.skill.take_action (and the plugin / mcp equivalents, true by default) controls only post-scan severity actions. With it false, those severity verdicts are recorded without their configured block, quarantine or disable actions. An explicit asset_policy.denied match and a scanner failure still fail closed: the watcher blocks the install and quarantines skill or plugin files. Do not use take_action: false as an observation-only install mode. When take_action is turned back on, the watcher restarts, and its first rescan runs admission again for each asset rejected while it was off and applies the actions, as for a new install. The watcher-block row ends with rejected while take_action was off, enforced once it was turned on. This needs the periodic rescan (watch.rescan_enabled, on by default). Until then a managed computer's status and verify warn with asset_rejected_not_enforced.

The admission block

admission: has one block per asset type: skill, mcp and plugin. There is also a defaults block. Each asset type inherits defaults field by field: a field you leave out of skill falls back to defaults, then to the built-in value.

There is no admission.tool. Nothing installs a tool, so there is nothing to scan; tools have name lists only.

admission:
  defaults:
    scan_on_install: true
    allow_list_bypass_scan: true
  skill:
    actions:
      critical: quarantine
      high: quarantine
      medium: warn
      low: allow
      info: allow
    scanner_overrides:
      skill-scanner:
        medium: block
    first_party_allow_list:
      - name: acme-review
        source_path_contains: [".acme/skills/acme-review"]
        reason: first-party Acme skill
  mcp:
    actions:
      high: block
      medium:
        install: none
        file: none
        runtime: disable
  plugin:
    allow_list_bypass_scan: false

Prop

Type

Actions

An action is a shorthand or the exact triple it stands for. Use the triple only when no shorthand fits.

ShorthandinstallfileruntimeVerdict
blockblocknonedisablerejected
quarantineblockquarantinedisablerejected
warnnonenoneenablewarning
allownonenoneenableallowed

In a triple, install is block, allow or none; file is none or quarantine; runtime is disable or enable. A finding is rejected whenever install is block or runtime is disable. warn and allow do the same thing to the asset; they differ only in the verdict they record. A severity that no layer covers fails closed, as a block.

Quarantine moves the files of a skill or plugin into DefenseClaw's quarantine area. An MCP server has no files to move, so for MCP block and quarantine both mean: refuse its tool calls.

What applies if you set nothing

Each severity resolves on its own, first match wins:

  1. admission.<type>.actions
  2. For skills only, the scanner gate (below)
  3. admission.defaults.actions
  4. The built-in action
SeveritySkill (default gate)MCP serverPlugin
criticalquarantinequarantinequarantine
highquarantinequarantinequarantine
mediumwarnquarantinewarn
lowallowinstall none, file none, runtime disablewarn
infoallowwarnwarn

The built-in first-party entries are codeguard for skills, defenseclaw for plugins, and none for MCP servers. Both names are DefenseClaw's own, so they trust only DefenseClaw's own content; see First-party assets.

The scanner gate

A skill's actions derive from the skill scanner's own gate unless admission.skill.actions sets them. scanners.skill_scanner.fail_on_severity (default HIGH) and scanners.skill_scanner.review_queue_min (default MEDIUM) give the rule: a finding at or above the gate quarantines, a finding from the review minimum up to the gate warns, and anything lower is allowed. Raising the gate in one place therefore changes what a skill scan does without touching admission:.

defenseclaw config set scanners.skill_scanner.fail_on_severity CRITICAL
defenseclaw config set scanners.skill_scanner.review_queue_min LOW
defenseclaw config get admission.skill.actions --effective
Set scanners.skill_scanner.fail_on_severity (config generation 6, sha256 295115627558).
Effective policy digest: sha256:37484d42a2a3b4428f4b592f5a33bdf27bfad89a8b81005534bc0e388bdef510
Set scanners.skill_scanner.review_queue_min (config generation 7, sha256 db97f7544649).
Effective policy digest: sha256:8247599d5d5d1b6a8b8dc84a1b9ff4c88669c653805559fa6889daf91a315f9f
(source: derived:scanners.skill_scanner)
critical: quarantine
high: warn
info: allow
low: warn
medium: warn

The gate shadows admission.defaults for skills

Only skills have a scanner gate, and it sits between admission.skill and admission.defaults. So admission.defaults.actions never changes a skill's actions: set admission.skill.actions for that. MCP servers and plugins have no gate, so they do follow admission.defaults.actions.

A severity you set under admission.skill.actions wins for that severity only; the others keep deriving from the gate:

defenseclaw config set admission.skill.actions.medium block
defenseclaw config get admission.skill.actions --effective
Set admission.skill.actions.medium (config generation 4, sha256 621b671d0367).
Effective policy digest: sha256:995422aed39baf2e53ad6ac0f3b8e0a489696ea70e21009a04422d210c748fea
(source: config:admission.skill.actions)
critical: quarantine
high: quarantine
info: allow
low: allow
medium: block

The generation numbers and digests in these examples differ on your machine. The (source: ...) line goes to stderr and prints first; it names the highest-priority layer that set any of the severities: builtin, derived:scanners.skill_scanner, config:admission.<type>.actions or config:admission.defaults.actions.

Scanner overrides

scanner_overrides changes the action for one scanner's findings. The key is the name of the scanner that produced the result, not an asset type. This blocks a skill whose worst finding is MEDIUM when the skill scanner found it, and leaves the action for findings from other scanners alone:

defenseclaw config set admission.skill.scanner_overrides.skill-scanner.medium block

First-party assets

A first-party entry trusts an asset by name and location. It matches when the asset's name equals name and its path contains one of the source_path_contains values as whole path components, ignoring case and using either slash. A look-alike never matches:

Asset pathMarker .acme/skills/acme-reviewVerdict
/home/alice/.acme/skills/acme-reviewmatchesallowed, scan skipped
/home/alice/.acme/skills/acme-review-evilno: a different last componentscan
/home/alice/.acme-evil/skills/acme-reviewno: a different first componentscan
/Users/alice/.ACME/Skills/acme-review/SKILL.mdmatches (case does not matter)allowed, scan skipped
defenseclaw config set admission.skill.first_party_allow_list --json '[{"name":"acme-review","source_path_contains":[".acme/skills/acme-review"],"reason":"first-party Acme skill"}]'

codeguard and defenseclaw are DefenseClaw's own asset names, and a folder in a skills or plugins folder can be written by anything the user installs (a git clone, npx skills add). So an entry with one of these names, built in or set by you or a policy profile, never trusts a folder by its name and location alone:

  • codeguard (skill) skips the scan only for an exact copy of the CodeGuard skill DefenseClaw ships, checked by a SHA-256 digest of its files. A folder named codeguard with any other content is scanned like any other skill.
  • defenseclaw (plugin) skips nothing by itself. The gateway recognizes DefenseClaw's own plugin by its bytes (the OpenClaw plugin it installs, the Amp bridge file) before admission; any other plugin with that name is scanned.

Entries with other names match by name and location, as above.

The list you set replaces the built-in one for that asset type, so list codeguard again if you still want it trusted. --json '[]' trusts no asset. The reason is a note for people and is not evaluated. Turn first-party trust off for one type with allow_list_bypass_scan: false; the entries then stay in the file but every asset is scanned.

Check what applies

Read the compiled result for one type, with the layer it came from:

defenseclaw config get admission.skill --effective
(source: derived:scanners.skill_scanner)
actions:
  critical: quarantine
  high: quarantine
  info: allow
  low: allow
  medium: warn
allow_list_bypass_scan: true
first_party_allow_list:
- name: codeguard
  source_path_contains:
  - .openclaw/workspace/skills/codeguard
  - .openclaw/skills/codeguard
  - .zeptoclaw/skills/codeguard
  - .claude/skills/codeguard
scan_on_install: true
scanner_overrides: {}

Dry-run one asset through the running policy with the gateway CLI. This asset is on the block list, so it never reaches a scan:

defenseclaw-gateway policy evaluate --target-type skill --target-name risky-skill
{
  "verdict": "blocked",
  "reason": "skill 'risky-skill' is on the block list",
  "file_action": "none",
  "install_action": "none",
  "runtime_action": "allow"
}

Add --severity and --findings to simulate a scan result:

defenseclaw-gateway policy evaluate --target-type skill --target-name new-skill --severity HIGH --findings 3

On a managed Linux or macOS host, run the gateway command as an administrator; it reads the managed configuration. On a managed Windows standalone host, use an elevated PowerShell prompt and the installed CLI:

& 'C:\Program Files\Cisco\DefenseClaw\bin\defenseclaw.exe' policy evaluate --target-type skill --target-name risky-skill

The managed command reads the administrator's policy. A standard account cannot use its own ~/.defenseclaw/config.yaml to check the managed verdict.

{
  "verdict": "rejected",
  "reason": "max severity HIGH triggers block per policy",
  "file_action": "quarantine",
  "install_action": "block",
  "runtime_action": "block"
}

In the output, runtime_action uses the policy engine's spelling, block or allow, for the config's disable or enable. The dry run has no connector and uses the path /dry-run, so rules scoped to a connector and allow rules pinned to a path do not match in it. defenseclaw-gateway policy show prints the whole compiled admission policy.

The block/allow list

asset_policy is the only block list and the only allow list. For each of skill, mcp and plugin, asset_policy.<type>.denied and asset_policy.<type>.allowed hold the rules:

asset_policy:
  skill:
    denied:
      - name: risky-skill
        reason: unvetted vendor skill
    allowed:
      - name: vetted-skill
        reason: reviewed by security
        source_path_contains: ["/opt/acme/skills/vetted-skill"]
  mcp:
    denied:
      - name: evil-mcp
        connector: claudecode
        reason: exfiltrates data
      - url: https://mcp.example.net/v1
        reason: unknown vendor
  tool:
    denied:
      - name: delete_file
        connector: claudecode
        reason: destructive
    allowed:
      - name: read_file
        reason: vetted

Prop

Type

A rule needs at least one of these fields to match anything. There are no wildcards or regular expressions, because this list gates code and tool execution.

Which rule wins

  1. A rule scoped to the asset's connector is checked before an unscoped one. A connector-scoped allow therefore overrides a global deny for that connector.
  2. At the same scope, denied is checked before allowed.
  3. The first matching rule decides.

Path pins

source_path_contains ties a rule to one on-disk copy. An allowed rule only matches when its pin appears in the asset's path as whole path components, so an allow for /opt/acme/skills/vetted-skill never covers /opt/acme/skills/vetted-skill-v2, and an allow with a pin never matches an asset that comes with no path. A denied rule with a pin is looser: its text matches as a case-insensitive substring of the path, and it matches nothing when no path is presented. The CLI writes a pin when you allow an installed skill or plugin, and never writes one on a block, so a block follows the name wherever the asset is installed.

enabled and mode

The explicit lists always apply, whatever asset_policy.enabled and asset_policy.mode say. Those two keys (enabled: false and mode: observe by default) govern only the other asset policy rules: a default: deny for a type, registry_required, and runtime detection. With mode: observe such a rule records that it would have blocked and lets the asset through; with mode: action it blocks. See Registries for the approved-catalog rules.

asset_policy.<type>.runtime_detection works the same way. With runtime_detection.enabled: false (the default for skills and plugins) an agent hook still refuses a skill, plugin command or MCP server on the denied list; only the default, registry and approval rules are skipped at the hook. Those rules then apply only where the install watcher admits the asset (per-user installs and managed Windows). A managed Linux or macOS device runs no install watcher for skills and plugins, so there a default: deny or registry_required for them blocks nothing until you set runtime_detection.enabled: true; ensure, status and verify warn with asset_policy_not_enforced, and the gateway log says so when it starts.

Where a denied skill is refused

An agent hook refuses a denied skill when the agent selects it: Claude Code's Skill tool and slash commands, and Codex's $name selection. A denied name also matches the copy a Claude Code plugin bundles (<plugin>:<name> at the Skill tool and /<plugin>:<name>). Codex has no hook on its own skill loading, so asked for a skill in plain words it reads the skill's SKILL.md with a shell command and follows it. The Claude Code and Codex hooks therefore also refuse any tool call that reaches into the folder of a denied skill: a .../skills/<name>/... path in a command, a file path or a working folder. A skill that install admission blocked and disabled (one whose quarantine failed stays in its folder) is refused the same ways, also when Codex selects it by the name its SKILL.md declares rather than its folder name. Two things stay out of reach of a hook:

  • the skill's name and description, which Codex lists to the model from its own skill index before any hook runs;
  • a command that builds the path at run time, for example from a variable, a glob, or a cd into the skills folder followed by a relative path.

Project skill folders (<project>/.claude/skills, <project>/.agents/skills, <project>/.codex/skills) are watched once an agent session runs in the project: its first hook (the session start, or the first skill call) adds the project's existing skill folders to the install watcher, which restarts and admits what they hold, as at a gateway start. Until a project skill's first scan has finished, the hooks refuse it with "is not scanned yet ... try again in a minute". Up to 32 project folders are watched; on a managed computer only projects inside the user's home count, and explicit gateway.watcher.skill.dirs add none. The hooks also cover them as they cover every other folder: a denied name is refused at the Skill call, the $name selection and the folder read, wherever the skill lives. With runtime_detection.enabled: true, mode: action and a default: deny or an approved registry, the Skill call and $name selection of any project skill that is not approved are refused too.

The folder rule covers the denied list only. A skill that only a default: deny or registry_required keeps out is refused at the Skill call and the $name selection, but not when Codex, asked for the skill in plain words, reads its SKILL.md and follows it. To keep such a skill from running in Codex, add it to asset_policy.skill.denied or take it off the device.

Take a denied skill off the device to remove it from that index. Where the install watcher runs (per-user installs and managed Windows), a config change that adds a denied entry makes it check the installed skills, plugins and MCP servers at once: one the entry names is refused (a skill or plugin is quarantined), with an install-rejected audit row, without waiting for its files to change. An MCP server is matched on its definition too, so a command or url rule added later rejects a server that was already installed; the hooks refuse its tool calls from the moment the config applies. On other managed devices remove it with your device management tool; the hooks refuse it in the meantime.

A Claude Code plugin is refused at the hook as well. Claude Code runs a plugin installed from a local marketplace folder (claude plugin marketplace add <folder>) from that folder, not from its copy under ~/.claude/plugins/cache, and DefenseClaw never changes a marketplace folder, which other users may share. When asset_policy.plugin denies a plugin, or admission blocked it, the cache copy is quarantined as usual and the Claude Code hook refuses the plugin's skills and commands (/<plugin>:<skill>), its agents (<plugin>:<agent>) and its MCP tools (mcp__plugin_<plugin>_<server>__<tool>). The hook sees the plugin name only, so a denied rule written as plugin@marketplace refuses that plugin from any marketplace. claude plugin list keeps showing such a plugin as enabled, since that list is Claude Code's own record and DefenseClaw does not edit it; defenseclaw plugin list shows the block.

The MCP servers a Claude Code plugin bundles are admitted with their plugin: they get no admission row or MCP inventory entry of their own. The hooks hold their tool calls to asset_policy.mcp all the same, under the name plugin:<plugin>:<server> and by their command, args_prefix or url.

Tool lists

asset_policy.tool.denied and asset_policy.tool.allowed hold rules with a name, an optional connector and a reason. Tool names match case-sensitively. For a tool call from connector C, the order is: denied for C, allowed for C, denied for any connector, allowed for any connector, then the normal guardrail checks. An allow skips the rule, pattern and judge checks, but a tool that writes files still goes through CodeGuard.

Change the lists

WriterUse it for
defenseclaw skill, mcp, plugin, tool block, allow, unblockThe everyday way. Each command edits the list and the gateway applies it.
defenseclaw config set asset_policy...Anything the commands do not cover: URL or command rules, path pins, bulk edits.
REST POST and DELETE /enforce/block, POST /enforce/allowScripts and other tools. See Gateway API.
Editing config.yaml by handWorks. The gateway applies a valid change within seconds and rejects an invalid one. config set checks the change before it writes.

Every writer goes through the one config writer, and the gateway applies the change without a restart. A change takes effect on the next decision; there is nothing to reload.

defenseclaw skill block risky-skill --reason "unvetted vendor skill"
defenseclaw mcp block evil-mcp --connector claudecode --reason "exfiltrates data"
defenseclaw plugin block shady-plugin --reason "unsigned"
defenseclaw tool block delete_file --connector claudecode --reason "destructive"
defenseclaw tool allow read_file --reason "vetted"
defenseclaw tool list
[skill] Blocked 'risky-skill' (every connector).
[mcp] Blocked 'evil-mcp' (claudecode).
  Note: 'evil-mcp' is not configured on claudecode. Configured MCP servers: none.
  The block applies if a server with this name or URL is added later.
  Check the name with: defenseclaw mcp list --connector claudecode
[plugin] Blocked 'shady-plugin' (every connector).
[tool] 'delete_file' added to block list (connector 'claudecode')
[tool] 'read_file' (covers connector=claudecode) added to allow list
Tools (connector=claudecode)
TOOL         STATUS   SCOPE      REASON                                    UPDATED
----------------------------------------------------------------------------------
delete_file  block    connector  destructive                               2026-10-06 18:19
read_file    allow    global     vetted                                    2026-10-06 18:19
  • Without --connector, a block or allow covers every connector. With --connector, it covers that one. skill allow and plugin allow on an installed copy write a rule scoped to the connector that has the copy and pinned to its path. skill allow on a quarantined copy pins the rule to the path the copy is restored to, and says the files stay in quarantine until defenseclaw skill restore <name>. When there is no copy to pin to, the command says the rule matches the name only.
  • mcp allow pins the rule to how the server starts: its url, or its command and args_prefix (and transport when the server sets one), read from the connector's MCP config. A different server added later under the same name is scanned again. For a server that is not configured (for example one mcp set just rejected), pass the definition you reviewed: defenseclaw mcp allow context7 --command npx --args '["-y", "@upstash/context7-mcp"]'. Without a configured copy or a pin the rule matches the name only, and mcp allow says so.
  • skill allow and plugin allow remove the name from the block list as well.
  • unblock removes the block or allow rule for the name and clears its runtime state, so its scan verdict decides again: the CLI says the asset is scanned on the next check. It never restores files; use restore for that.
  • A block does not touch an asset that is already running. Use disable or quarantine for that.
  • With the gateway stopped, the commands still write config.yaml and warn that no audit event was recorded. The gateway applies the list when it starts.

Over REST, state-changing calls need the X-DefenseClaw-Client header and a JSON content type, besides the gateway token:

curl -s -X POST http://127.0.0.1:18970/enforce/block \
  -H "Authorization: Bearer $DEFENSECLAW_GATEWAY_TOKEN" \
  -H "X-DefenseClaw-Client: cli" \
  -H "Content-Type: application/json" \
  -d '{"target_type":"skill","target_name":"curl-skill","reason":"blocked from a script"}'
{"effective_policy_digest":"sha256:8e6d614e5be0460109785e8e0d532aa980ab41dd87babf936d2675c4726f0c4b","generation":17,"status":"blocked"}

Managed devices

On a managed standalone device the admin config is the only source. Every writer refuses, and the attempt is recorded in the audit log:

Error: This device is managed: add it to asset_policy in the admin config (MDM or management plane)

The CLI exits with code 3, and defenseclaw config set refuses with the message error: This device is managed: change config.yaml in the admin config (MDM or management plane), not on the device, also exit 3. REST answers 403 with {"error":"managed_device","detail":"policy changes are made in the management plane"}. Put the same asset_policy rules in the admin config. See the enterprise guide for how that config reaches devices. Reading the lists (GET /enforce/blocked and /enforce/allowed) works everywhere.

Secure Client devices are unchanged: they keep reading the version 8 admission policy and keep their operator entries in the audit database. See Secure Client.

Users and groups

Admission and the block/allow lists belong to the device. A rule can be scoped to a connector, but not to a user or a group. Guardrail profiles are what you target at users and groups, and they carry guardrail settings (mode, thresholds, human approval, rule pack), not admission. See User- and group-based policies.

What stays runtime state

Some enforcement results are records of what happened on one machine, not intent. They stay in the actions table of audit.db, the enforcement journal, and are never read as policy:

  • quarantine and restore of a skill's or plugin's files (quarantine, restore)
  • runtime disable and enable (disable, enable)
  • the install block that a scan verdict records (its reason starts with auto-block, post-scan: or similar)

The journal is not part of the effective policy digest, and config.yaml never lists it. unblock also clears the journal's install block for the name, so a later restore does not leave the asset blocked. On a managed device, which has no enable or restore, an allowed rule for a skill or plugin in the admin config clears its install block and runtime disable at the next admission or rescan. See Release a blocked or quarantined skill.

Upgrading from config_version 8

defenseclaw config migrate (and defenseclaw upgrade, which runs it) moves the old admission inputs into config.yaml. --dry-run shows what would move and writes nothing. The migration keeps config.yaml.v8.bak and writes migration-v9.json. Until you run it, the version 9 gateway reads a version 8 file in memory, with the old inputs still applying; after it, those keys are rejected in a version 9 file.

Old inputBecomes
policies/rego/data.json config.scan_on_install and allow_list_bypass_scanadmission.defaults.*, written only when the value was off
data.json actions and the per-type scanner_overridesadmission.<type>.actions, a full five-severity table, written only for a type that differs from the built-in
data.json first_party_allow_listadmission.<type>.first_party_allow_list, written only when it differs from the built-in
skill_actions, mcp_actions, plugin_actionsRemoved. They never changed an outcome for the severities data.json covered. A value you had customised away from what data.json enforced is listed as a conflict in migration-v9.json.
watch.allow_list_bypass_scanRemoved. Nothing read it.
Operator rows of the audit actions tableasset_policy.<type>.denied and allowed
data.json itselfRenamed data.json.migrated-v9

An operator row is an actions row whose install field is block or allow, unless its reason is a scan verdict (it starts with auto-block, post-scan:, post-install scan:, scan: or scan clean or within policy). Each moved row becomes a rule with its name, connector and reason. An allow also keeps its source_path as a source_path_contains pin; a block does not. A tool row named @connector/tool becomes a rule for tool on that connector, and a row named source/tool drops the source. After the config is written, the migration clears the install field of every moved row, and deletes a row that has nothing else in it. Quarantine and disable state, scan-verdict blocks and rows of an unknown type stay in the table.

A dry run on a version 8 home whose actions table holds five operator rows (a1 to a5) and three journal rows (a6 to a8: a scan-verdict block, a quarantine and a watcher auto-block):

defenseclaw config migrate --dry-run
Would move 6 values into config.yaml; 0 conflicts.
  scanners.skill_scanner.policy -> scanners.skill_scanner.policy
  actions:a1 -> asset_policy.skill.denied
  actions:a2 -> asset_policy.skill.allowed
  actions:a3 -> asset_policy.mcp.denied
  actions:a4 -> asset_policy.tool.denied
  actions:a5 -> asset_policy.tool.denied

On a managed standalone device nothing moves. The migration only counts the local rows, ignores them, and reports a local_enforcement_entries_ignored warning with the count; the admin config is the policy. On a Secure Client device they stay in the table, unchanged.