Claude plugin validate passes a permissionMode the flag already refuses

John Burns

permissionMode: yolo did not fail in one place. On Claude Code 2.1.308 it failed in the --agents flag, was logged and then ignored as a process result for a project agent file, and was loaded on purpose for a plugin agent. claude plugin validate passed the directory that held the illegal file. A job that treats that pass as a mode check is checking a different gate.

The three results are not three prints of one rule. The flag exits before it looks for a key. The project file writes a debug line and continues. The plugin file warns that the field is ignored, then loads the agent. Validate does not read the field in any of those files.

What the binary was asked to do

The binary was Claude Code 2.1.308 on Linux x86_64. claude --version printed 2.1.308 (Claude Code). The run was 6 November 2026. Each case used a new CLAUDE_CONFIG_DIR. The normal Claude home was not the directory under test. No login was run. A non-working API key was set in the process environment so that a session which got past the mode gate would stop on Invalid API key · Fix external API key instead of sending a prompt. No model reply was accepted as evidence. No subagent was invoked. Nothing below says a delegation used the mode, or that a later tool call was denied.

The subagents page, read the same day, lists default, acceptEdits, auto, dontAsk, bypassPermissions, and plan, and says manual is an alias for default. It says a parent session in bypassPermissions, acceptEdits, or auto mode ignores the permissionMode set on the subagent. It does not say a plugin agent ignores the field when the parent is dontAsk. The plugin manifest page says an agents entry is a path to a .md file, and that a directory is not accepted. claude plugin validate --help says the command validates a plugin or marketplace manifest, or the skills, agents, and commands in a directory. Those sentences were the baseline. The runs below are the parts those sentences do not print.

The flag refuses the value

An inline --agents object with permissionMode set to yolo exited 1 before any key check. Stdout was empty. Stderr was:

Error: Invalid --agents configuration:
reviewer.permissionMode: Invalid option: expected one of "acceptEdits"|"auto"|"bypassPermissions"|"default"|"dontAsk"|"plan"

The same object in a JSON file produced the same two lines, then a third line, (read from <path>), naming that file. The refusal is the configuration parser. It does not depend on a project directory, a plugin manifest, or a debug log.

A file with permissionMode set to dontAsk did not print that error. The process reached Invalid API key · Fix external API key on stdout and exited 1. A file with permissionMode set to manual did the same. The refusal text does not list manual. The parser accepted it anyway. This run did not print the mode the session resolved, so it does not say manual became default. It says a value can pass the flag and still be absent from the list the flag prints when it refuses a different value. A job that greps the error string for the complete set of accepted names will miss manual.

The flag is the only one of these checks that makes an illegal mode the process result. If the mode is supplied on the command line, or in the JSON file that --agents reads, this is the check to require. Exit 1 with that field name is the failure. A later key error means the mode value got through.

The project file logs the list and continues

A project file at .claude/agents/bad-mode.md used the same permissionMode: yolo frontmatter, with name, description, tools, and model set. The session was started in that project, without --bare, with --debug-file, --permission-mode dontAsk, and --permission-prompts none. Stderr was empty. Stdout was the key error. The debug log contained one line for the file:

Agent file .claude/agents/bad-mode.md has invalid permissionMode 'yolo'. Valid options: acceptEdits, auto, bypassPermissions, default, dontAsk, plan

That line is debug, not the error the operator sees. The valid options match the flag’s list, including the omission of manual. The next debug line was Total plugin agents loaded: 0, which is a plugin count, not a statement that the project file was dropped. A second project, whose only agent file set permissionMode: dontAsk, produced no permissionMode line and the same key error on stdout. This run did not record a later “loaded” line for either project file. It therefore does not say the illegal file was omitted from the agent list, and it does not say a delegation would have failed. The measured split is narrower: the illegal project file is named in the debug log, and that name never becomes the process result.

--bare changes the observation. The same project file, started with --bare, did not produce the invalid-mode line. The help text for --bare says it skips hooks, plugin sync, and CLAUDE.md auto-discovery, and that skills still resolve. It does not say project agent files are skipped. On this binary the mode line was absent under --bare and present without it. A headless probe that adds --bare to keep the run small will not see this refusal. Drop --bare when the question is whether a project agent file was read.

claude plugin validate on that .claude/agents directory printed Validating components in: and then Validation passed, and exited 0. The same command on the directory whose file used dontAsk printed the same pass. Passing the markdown file itself, rather than the directory, did not check frontmatter. It treated the file as a plugin manifest and exited 1:

json: Invalid JSON syntax: JSON Parse error: Invalid number

A plain directory of agent markdown, with no .claude-plugin/plugin.json, failed the other way: No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json. The help sentence that says validate checks agents in a directory is true only for a directory the command already recognizes as components. .claude/agents is one of those directories, and the pass does not include permissionMode. A markdown path is not an agent check. It is a JSON parse.

A plugin agent loads and ignores the field

A plugin whose manifest set agents to ./agents/bad-mode.md held the same illegal frontmatter. claude plugin validate printed Validation passed and exited 0. The human report listed a missing README as a directory finding and said those directory checks do not change the result unless the command is run with --profile directory. It did not mention the agent file, the frontmatter, or permissionMode. --strict on that plugin still exited 0. The field is not a warning that --strict promotes.

The print session, with --plugin-dir and without --bare, also did not refuse the value. Stderr was empty. Stdout was the key error. The debug log warned, then loaded the file:

Plugin agent file agents/bad-mode.md sets permissionMode, which is ignored for plugin agents. Use .claude/agents/ for this level of control.
Loaded agent from plugin probe-bad-agent custom file: agents/bad-mode.md
Total plugin agents loaded: 1

A second plugin, whose agent file set permissionMode: dontAsk, produced the same warning, the same load line, and Total plugin agents loaded: 1. The warning is not about yolo. It is about the field being present. A documented mode and an undocumented mode take the same path: the agent loads, and the mode is not applied. The subagents page describes cases where a parent mode overrides a subagent mode. This warning is a different rule. It tells you to put the field in .claude/agents/ if you want that control, and it does so only in the debug log.

An empty agent file is the case validate does read. Declaring ./agents/empty.md with no frontmatter put the file in the contents report and warned No frontmatter block found. Without --strict that warning still exited 0. With --strict the command exited 1. So the content scan can see an agent file, and a missing frontmatter block is a warning. An illegal permissionMode inside a frontmatter block was not reported at all. A file whose frontmatter YAML did not parse was also absent from contents, and validate exited 0. Do not treat an empty contents array as proof that every declared agent was checked and found clean. On this binary it also means the file was not listed.

A missing declared path is the failure validate does make. agents set to ./agents/missing.md exited 1 with:

agents[0]: Path not found: ./agents/missing.md. The runtime loader will report this as a load failure.

The print session for that plugin did log an error: Agent path ./agents/missing.md specified in manifest but not found. It then logged Loaded inline plugin from path: probe-missing-agent and still ended on the key error. The loader sentence is true of the debug log. It is not true of the visible result of claude -p. A job that only checks the print exit, or only greps stdout for a missing-file string, will not see the failure validate already printed. Read the debug file if you need the loader line. Require validate’s exit 1 if you need the missing path to fail the job.

A directory path in agents fails earlier, and for a different reason. "./agents/" exited 1 with agents.0: Invalid string: must end with ".md". That matches the manifest page. It is not a permission-mode result. A job can have a green mode story and still be pointing the manifest at a directory.

Verify which gate read the mode

Confirm claude --version before comparing a result with this note. Use a disposable CLAUDE_CONFIG_DIR. Do not point the run at the operator home. Set a key you expect to fail if you are testing the gate rather than a model call, and do not treat Invalid API key as a mode result.

For a mode passed on the command line, require the flag to refuse an illegal value before the key check:

claude --agents '{"reviewer":{"description":"Reviews a diff.","prompt":"Report defects only.","permissionMode":"yolo"}}' -p "ping"

Expect exit 1, empty stdout, and reviewer.permissionMode: Invalid option on stderr. The choice list on this binary is the six names above. If the value is manual, expect the process to continue. Absence from that list is not a refusal. Repeat the check with the JSON file form if a job reads the mode from disk. The extra line should name the file.

For a project agent, write the file under .claude/agents/, start in that directory, and do not pass --bare. Capture --debug-file. An illegal value should appear as the debug line that names the file and the valid options. It will not appear on stderr. A pass from claude plugin validate on that directory does not replace the debug line. Pointing validate at the markdown file checks JSON syntax, not frontmatter. If the debug line is missing, the file was not read under the flags you used. --bare was enough to hide it in this run.

For a plugin agent, do not put permissionMode in the plugin file if the mode has to hold. The warning says to use .claude/agents/ for that control, and both a legal and an illegal value were ignored after the warning. Require the warning when the field is present, and require Total plugin agents loaded to match the files you meant to load. Use claude plugin validate for the manifest path: a missing .md file must exit 1, and a directory path must fail the .md suffix check. Do not use validate’s Validation passed as evidence that the mode was read. --strict did not change that for the illegal field. --profile directory changes a different result, the catalog submission findings, and still does not read permissionMode.

Stop there. This note does not say which mode a spawned subagent actually ran under, because no spawn was completed. The parent mode can still override a project file, which the subagents page already describes. The checks above answer a prior question: which command reads the field, which command only logs it, and which command passes without opening it.

Sources

  • Claude Code 2.1.308 (claude --version printed 2.1.308 (Claude Code)) on Linux x86_64, 6 November 2026. Isolated CLAUDE_CONFIG_DIR. No login and no accepted model reply. Commands were claude --agents with an inline object and with a JSON file, claude -p in a project that held .claude/agents/*.md, claude -p --plugin-dir against plugin directories, and claude plugin validate with and without --strict and --profile directory.
  • Claude Code subagents page, read 6 November 2026: Create custom subagents. It lists the permission modes, the manual alias, and the parent modes that ignore a subagent permissionMode. It does not describe the plugin-agent ignore warning.
  • Claude Code plugin manifest page, read 6 November 2026: Plugin manifest reference. It says an agents path must be a .md file and that a directory is not accepted. It describes claude plugin validate results for manifest errors and warnings.