Skip to content

Agent rules and opt-in rule mechanism

DESIGN 0005: Agent rules and opt-in rule mechanism

Section titled “DESIGN 0005: Agent rules and opt-in rule mechanism”

Status: Approved Author: Donald Gifford Date: 2026-07-10

Agents are the last artifact kind with zero rules: the docs define a 16-field frontmatter spec with at least three silent-failure behaviors (typo’d model silently inherits, unknown tools entries silently ignored, plugin agents silently dropping mcpServers/hooks/ permissionMode). This design specifies the extended Agent artifact model, the five always-on agent rules (IMPL-0004 Phase 4), the engine-level opt-in mechanism resolved as IMPL-0004 OQ4 option (a), and the opt-in agents/model-policy rule (Phase 5).

Approval provenance: OQ4 was resolved by Donald on 2026-07-10 as option (a) — engine-level, config-driven enable — in IMPL-0004 Resolved Decisions; this document details that mechanism.

  • Parse the documented subagent frontmatter fields needed by Phases 4–5.
  • Specify five always-on agent rules precisely enough to implement without further design decisions.
  • Define one house convention for opt-in rules, enforced by the engine, and migrate mcp/server-allowlist onto it.
  • Share the model-value validator across agent, skill, and command model fields.
  • Validating mcpServers / hooks contents inside agent frontmatter (presence is enough for Phase 4; deep validation can reuse the MCP and hook rule packages in a later phase).
  • Cross-file analysis (e.g. does skills: reference an existing skill, duplicate agent names across scopes). Rules stay pure over one artifact.
  • initialPrompt content linting.
  • A general plugin system for third-party rules — opt-in rules are still built-in (DESIGN-0001 non-goal unchanged).

INV-0006 audited the ruleset against the current Claude Code docs and found the agent gap: a 16-field documented spec (subagents reference, fetched 2026-07-10) against a parser that reads three fields. The proposed rules and their phases come from INV-0006’s “Agents” table; the open mechanism question (how does a rule ship disabled-by-default?) was IMPL-0004 OQ4.

Prior art in this repo: mcp/server-allowlist implemented opt-in inside the rule — default-enabled, DefaultOptions declaring a nil allowlist, loud config-error diagnostic when unconfigured. CLAUDE.md explicitly said adding an engine hook for one rule would be over-engineering and to revisit when a second rule needed the pattern. agents/model-policy is that second rule.

The subagents reference documents 16 frontmatter fields. ParseAgent extends to the following. Every key’s range is available via Frontmatter.KeyRange(key) — the frontmatter parser already records byte-accurate ranges for all keys, so no new range plumbing is needed; rules anchor diagnostics per the range-emission conventions.

FieldParsed asNotes
nameName stringalready parsed
descriptionDescription stringalready parsed
toolsTools []stringalready parsed; via shared tool-list splitter
disallowedToolsDisallowedTools []stringvia shared tool-list splitter
modelModel stringraw declared value; absent = inherit
permissionModePermissionMode stringignored for plugin agents (rule fodder)
maxTurnsMaxTurns int640 = absent or non-numeric; presence via KeyRange
skillsSkills []stringverbatim string list (skill names, not tool syntax)
mcpServersHasMCPServers boolpresence only; value shape (name refs or inline configs) not modeled in Phase 4
hooksHasHooks boolpresence only
memoryMemory stringenum user/project/local
backgroundBackground boolabsent = false (“Claude chooses”); declared-vs-absent via KeyRange if ever needed
effortEffort stringenum low/medium/high/xhigh/max
isolationIsolation stringenum: worktree only
colorColor stringenum of 8 values
initialPromptInitialPrompt stringparsed for completeness; no Phase 4/5 rule consumes it

agents/plugin-ignored-fields needs to know whether an agent file is plugin-distributed. Detection is a path heuristic applied at discovery time, following the IndexSkillCompanions precedent (the parser stays pure over bytes; the discovery/engine wiring enriches the artifact with filesystem context):

  • After a successful ParseAgent, discovery walks up from the agent file’s directory; if any ancestor (up to the scan root) contains .claude-plugin/plugin.json, it sets Agent.PluginDistributed = true.
  • .claude/agents/** under a plain project root does not match; a plugin repo layout (<root>/.claude-plugin/plugin.json + <root>/agents/*.md) and marketplace checkouts (plugins/<name>/.claude-plugin/plugin.json) do.

The flag is data on the artifact, so the rule stays pure and unit tests can set it directly.

One validator in internal/artifact (alongside KnownTools), consumed by agents/model-valid for agent, skill, and command model fields and by agents/model-policy option validation:

  • KnownModelAliases = sonnet, opus, haiku, fable.
  • IsValidModelRef(v string) bool — true when v is an alias, inherit, or matches the full-ID shape ^claude-[a-z0-9-]+$.
  • Empty string is the caller’s concern (absent key = inherit; rules skip it).

New package internal/rules/agents/, blank-imported by internal/rules/all/. All five are always-on. Diagnostics never use file-level (0,0) ranges.

RuleCategory / severityAppliesToFires whenRangeMessage sketch
agents/model-validschema / warningagent, skill, commandmodel declared, non-empty, and !IsValidModelRefKeyRange("model")model "sonet" is not a known value; want sonnet, opus, haiku, fable, inherit, or a full model ID (claude-...)
agents/name-formatschema / warningagentname non-empty and not ^[a-z]+(-[a-z]+)*$KeyRange("name")agent name "My_Agent" should be lowercase letters and hyphens (empties are schema/frontmatter-required’s job)
agents/tools-knownschema / warningagententry in tools/disallowedTools fails IsKnownTool and IsToolPatternKeyRange of the offending keyunknown tool "Wrte" in tools — Claude Code silently ignores unknown names
agents/plugin-ignored-fieldscontent / warningagentPluginDistributed and any of permissionMode / mcpServers / hooks declaredKeyRange of each offending key (one diagnostic per key)"permissionMode" is ignored for plugin-distributed subagents — dead config
agents/field-enumsschema / warningagentdeclared enum field outside its documented setKeyRange of the offending keypermissionMode "yolo" is not a documented value; want default, acceptEdits, auto, dontAsk, bypassPermissions, plan, or manual

Enum sets for agents/field-enums (from the subagents reference, 2026-07-10):

  • permissionMode: default, acceptEdits, auto, dontAsk, bypassPermissions, plan, manual (documented alias for default, v2.1.200+).
  • effort: low, medium, high, xhigh, max.
  • color: red, blue, green, yellow, purple, orange, pink, cyan.
  • isolation: worktree.
  • memory: user, project, local.

No options on any Phase 4 rule. agents/model-valid runs on skills and commands too (same pattern as commands/allowed-tools-known running on skills — the ID keeps its home package’s prefix; rules.md notes the extra kinds).

Per OQ4 option (a). An opt-in rule is fully skipped unless the user’s .claudelint.hcl carries a rule "<id>" block for it.

Mechanics:

  • New optional interface in internal/rules:

    // OptIn marks a rule as disabled unless the user's config
    // explicitly carries a rule block for it. The engine checks for
    // this interface when filtering active rules.
    type OptIn interface {
    OptIn() bool
    }
  • Engine rule filtering (Runner.activeRules): when a rule implements OptIn and OptIn() is true, the rule runs only if the config has a rule "<id>" block (any block — even empty). enabled = false inside the block still disables it. No block → skipped entirely, no diagnostics of any kind.

  • Config exposes HasRuleBlock(id string) bool to support the check.

  • claudelint rules marks these entries (opt-in); rules --json gains an additive "opt_in": true field (docs/rules-json-schema.md updated accordingly).

  • The fingerprint buffer gains optin=<bool> per rule so flipping a rule’s opt-in status is visible drift.

  • mcp/server-allowlist migrates: it implements OptIn. With no rule block it is skipped (previously: loud config-error per artifact). With a rule block but no allowlist option, the loud config-error diagnostic is preserved — an explicit enable without an allowlist is still a misconfiguration, not a silent no-op.

  • CLAUDE.md’s “opt-in rules are implemented inside the rule” note is superseded by this convention (updated in the same commit).

Governance rule, error severity, opt-in via §5.

Options (exactly one required):

  • require = "inherit" — every agent must inherit: compliant when model is absent or inherit; anything else fires.
  • allowlist = ["opus", "claude-sonnet-5", "inherit"] — declared model must be in the list; an absent model key is evaluated as inherit (the documented default).

Config-error diagnostics (loud, per artifact, mirroring server-allowlist): both options set, neither set, require set to anything but "inherit", or an allowlist entry failing IsValidModelRef. Applies to agents only in Phase 5; extending to skill/command model can ride a later minor once policy demand exists.

  • internal/artifact: Agent struct gains 12 fields (§1, Data Model below); KnownModelAliases, IsValidModelRef; agent enum sets exported for rule reuse (AgentPermissionModes, AgentEffortLevels, AgentColors, AgentMemoryScopes).
  • internal/rules: new OptIn interface; fingerprint buffer gains optin= component (one-time fingerprint change, acked in Phase 4).
  • internal/engine: opt-in filtering in activeRules; Config.HasRuleBlock.
  • internal/cli: rules command renders (opt-in); rules --json adds opt_in.
  • No CLI flag changes; no config schema changes beyond the existing rule block semantics.
type Agent struct {
Base
Frontmatter Frontmatter
Body diag.Range
Name string
Description string
Tools []string
DisallowedTools []string
Model string
PermissionMode string
MaxTurns int64
Skills []string
HasMCPServers bool
HasHooks bool
Memory string
Background bool
Effort string
Isolation string
Color string
InitialPrompt string
// PluginDistributed is set by discovery (not the parser) when the
// agent file lives under a root containing .claude-plugin/plugin.json.
PluginDistributed bool
}
  • Parser: table-driven tests asserting every new field’s value; KeyRange presence for each declared key; absent-key zero values.
  • Rules: valid + invalid fixture pairs per rule; a plugin-rooted agent fixture (under testdata/.../.claude-plugin/) and an identical project-level agent proving plugin-ignored-fields scoping.
  • Opt-in engine behavior: engine-level tests — opt-in rule with no block (skipped), empty block (runs), block with enabled = false (skipped), server-allowlist block without allowlist (config error preserved).
  • Fingerprint: one deliberate ack when optin= joins the buffer.
  • Coverage: internal/rules/agents must clear the 55% floor.
  1. Phase 4 PR (minor release): parser extension + discovery flag + five always-on rules + the OptIn interface/engine support + mcp/server-allowlist migration. Release notes call out the server-allowlist behavior change: repos that never configured it stop seeing per-server config errors (they opted into nothing); repos with a block keep exact current behavior.
  2. Phase 5 PR (minor release): agents/model-policy on the now- proven mechanism, plus the remaining INV-0006 validation rules.

No data migration; .claudelint.hcl files need no edits.

None. OQ4 (the only contested mechanic) was resolved option (a) by Donald on 2026-07-10; this document is its write-up.