behavior.yaml Schema v1

April 13, 2026 · View on GitHub

YAML schema specification for dotforge v3.0 declarative behavior files. Enforcement semantics: SPEC.md. Design decisions: DECISIONS.md.


1. Overview

A behavior file declares an expected agent behavior, its enforcement policy, and its rendering templates. Each behavior lives in its own directory:

behaviors/
  <id>/
    behavior.yaml        # this schema
    tests/               # optional: test fixtures for validation

This directory-per-behavior structure allows test fixtures to live alongside the behavior without cluttering the root. The behaviors/index.yaml file controls which behaviors are active and in what evaluation order.

schema_version: "1" is required in every behavior file. It is the compile-time version of this schema, not the behavior's own version.


2. Complete Field Reference

schema_version: "1"          # string, required. Must equal "1".
id: search-first             # string, required. Kebab-case. Unique across all behaviors.
name: Search Before Writing  # string, required. Human-readable display name.
description: >               # string, required. 1-3 sentences stating the purpose.
  Require the agent to search existing code before writing new implementations.
  Prevents duplicate code and enforces codebase familiarity.
category: core               # enum [core, opinionated, experimental], required.
scope: session               # enum [session, task, project], required. Only "session" functional in 3.0.
enabled: true                # boolean, default true. Override per-entry in index.yaml.

policy:
  triggers:                  # array, required. At least one trigger.
    - event: PreToolUse      # enum [PreToolUse, PostToolUse, UserPromptSubmit, Stop], required.
      matcher: "Write|Edit"  # string, tool matcher pattern. Required for PreToolUse and PostToolUse.
                             # Optional for UserPromptSubmit and Stop.
                             # Examples: "Bash", "Grep|Glob", "*".
      conditions:            # array, optional. If empty or absent, any matching event triggers.
        - field: file_path   # string, from closed DSL field set (Section 3).
          operator: regex_match  # enum, from closed DSL operator set (Section 3).
          value: '\.(py|ts|js|swift|go|rs|java|kt)$'
      logic: all             # enum [all, any], default "all". How conditions are combined.
      action: evaluate       # enum [evaluate, set_flag, check_flag], default "evaluate".
                             # "evaluate" = increment counter + resolve level + emit output (standard path).
                             # "set_flag" / "check_flag" = flag-based temporal behaviors (Section 3.5).

  enforcement:
    default_level: silent    # enum [silent, nudge, warning, soft_block, hard_block], required.
    escalation:              # array, optional. Absent = always default_level.
      - after: 1             # integer >= 1, required. counter >= this value triggers level.
        level: nudge         # enum [silent, nudge, warning, soft_block, hard_block], required.
      - after: 3
        level: warning
      - after: 5
        level: soft_block

  recovery:
    hint: "Use Grep or Glob to search for existing patterns before writing new code."
                             # string, required. Instruction shown to agent on violation.
    suggested_tool: Grep     # string, optional. Tool name to suggest.
    suggested_action: "grep -r '<pattern>' src/"
                             # string, optional. Concrete command or action.

rendering:
  nudge_template: "{behavior_name}: Consider searching first (violation {counter})"
                             # string, max 120 chars. Supports {variables} (Section 4).
  warning_template: |
    **[{behavior_id}]** You have written code {counter} times without searching first.
    Expected: use Grep/Glob to find existing patterns before implementing.
    Action: search for related code, then proceed.
    Next violation triggers a block.
                             # string, max 500 chars. Supports {variables}.
  block_reason: "Must search the codebase before writing new code."
                             # string, max 200 chars.
  override_prompt: "Run Grep or Glob first, then retry the write operation."
                             # string, optional. Shown when soft_block allows override.

applies_to:
  tools: []                  # string array, optional. Tool names. Empty = all tools.
  agents: []                 # string array, optional. Agent names, simple string match. Empty = all.
  profiles: [standard, strict]
                             # string array, optional. Hook profiles [minimal, standard, strict].

metadata:
  author: dotforge           # string, optional.
  version: "1.0.0"           # string, optional. Semver.
  tags: [search, quality]    # string array, optional.

Field constraints summary

FieldTypeRequiredDefaultConstraint
schema_versionstringyesMust equal "1"
idstringyesKebab-case: [a-z][a-z0-9-]*[a-z0-9]
namestringyes
descriptionstringyes1–3 sentences
categoryenumyescore, opinionated, experimental
scopeenumyessession, task, project (only session functional)
enabledbooleannotrueOverridden by index.yaml
policy.triggersarrayyesAt least 1 item
triggers[].eventenumyesPreToolUse, PostToolUse, UserPromptSubmit, Stop
triggers[].matcherstringconditionalRequired for PreToolUse/PostToolUse
triggers[].conditionsarrayno[]Each item needs field, operator, value
triggers[].logicenumnoallall, any
triggers[].actionenumnoevaluateevaluate, set_flag, check_flag
triggers[].flagstringconditionalRequired when action is set_flag or check_flag. Kebab-case or snake_case.
triggers[].on_presentenumconditionalRequired when action is check_flag. consume or keep.
triggers[].on_absentenumconditionalRequired when action is check_flag. skip or violate.
enforcement.default_levelenumyesOne of 5 levels
escalation[].afterintegeryes>= 1
recovery.hintstringyesShown to agent on violation
nudge_templatestringnoMax 120 chars
warning_templatestringnoMax 500 chars
block_reasonstringnoMax 200 chars

3. Closed DSL Specification

Conditions reference fields from two closed namespaces. No other fields are valid in 3.0.

tool_input fields

Available when the trigger event provides tool context (PreToolUse, PostToolUse):

FieldAvailable for toolsDescription
commandBashFull command string
file_pathWrite, Edit, ReadTarget file path
contentWrite, EditFile content or new text
old_stringEditText being replaced
patternGrep, GlobSearch pattern
queryWebSearchSearch query
urlWebFetchTarget URL
promptAgentAgent prompt text

session_state fields

Available in all trigger evaluations:

FieldTypeDescription
counterintegerCurrent violation count for this behavior in the current session

Only counter is available in 3.0. Additional session_state fields deferred to 3.1.

Operators

String operators (for tool_input fields):

OperatorSemantics
regex_matchValue is a regex; field must match
containsField contains value as substring
not_containsField does not contain value
equalsExact string equality
starts_withField starts with value
ends_withField ends with value

Numeric operators (for session_state.counter):

OperatorSemantics
gtGreater than
ltLess than
gteGreater than or equal
lteLess than or equal
equalsEqual to

Existence operators (for any field):

OperatorSemantics
existsField is present and non-empty
not_existsField is absent or empty

3.5 Trigger Actions

Every trigger has an action that determines what happens when its matcher + conditions match. Default is evaluate. Only three actions exist in v1 — no composite actions.

ActionEffect on counterEffect on levelEffect on flagsUse case
evaluateincrement by 1resolve from counter (SPEC.md §2)noneStandard violation path (the only path in pre-flag behaviors)
set_flagnonenonecreates or re-sets flag with current set_atMark that a precondition was met (e.g., "a search happened")
check_flagconditional — see on_absentconditionalconsumed or kept — see on_presentGate a subsequent action on a prior flag

set_flag

Required fields:

  • flag — name of the flag to set

Semantics: on matcher+conditions match, the named flag is created (or its set_at updated) in sessions.<id>.flags. No counter increment. No output. Does not cut the chain.

check_flag

Required fields:

  • flag — name of the flag to check
  • on_presentconsume (delete after reading) or keep (leave in place)
  • on_absentskip (pass through as no-op) or violate (increment counter + resolve level as if this were an evaluate action)

Both on_present and on_absent are mandatory — there is no default. Declaring them explicitly makes the behavior's semantics auditable from the YAML alone.

Restrictions (v1)

  • action is always a scalar string. Lists are not allowed. A single trigger cannot both set and check a flag — declare two separate triggers.
  • Flag names are free-form strings but should be descriptive. The compiler does not enforce a schema on flag names.
  • Flags are runtime-internal. The DSL cannot read flag state through conditions — flags are manipulated only via action: set_flag and action: check_flag.

4. Template Variables

Available in nudge_template, warning_template, block_reason, and override_prompt:

VariableValue
{behavior_name}Human-readable name (from name field)
{behavior_id}Kebab-case id
{counter}Current violation count for this behavior
{tool_name}Tool that triggered the violation
{level}Current effective level name (e.g., warning)
{threshold}Next escalation threshold count, or "max" if at highest level

Example: "{behavior_name}: violation {counter}/{threshold} — {level} active"


5. behaviors/index.yaml Format

Controls which behaviors are active and in what order they are evaluated. Order determines chain evaluation sequence (see SPEC.md Section 4).

schema_version: "1"
behaviors:
  - id: search-first
    enabled: true
  - id: verify-before-done
    enabled: true
  - id: no-destructive-git
    enabled: true
  - id: respect-todo-state
    enabled: true
  - id: plan-before-code
    enabled: false  # opinionated, opt-in
  - id: objection-format
    enabled: false  # opinionated, opt-in

Rules:

  • enabled here overrides the behavior file's own enabled field.
  • Every referenced id must have a corresponding behaviors/<id>/behavior.yaml.
  • Evaluation follows declaration order — put safety-critical behaviors first.

6. Validation Rules

Compile-time checks that must pass before hook generation:

  • schema_version must equal "1".
  • id must be unique across all behaviors in the index.
  • id must match [a-z][a-z0-9-]*[a-z0-9] (no uppercase, no leading/trailing hyphens).
  • At least one trigger is required per behavior.
  • matcher is required when event is PreToolUse or PostToolUse.
  • All field values in conditions must be from the closed DSL field set (Section 3).
  • All operator values must be from the closed DSL operator set (Section 3).
  • action must be one of evaluate, set_flag, check_flag. Default is evaluate.
  • When action is set_flag, flag is required.
  • When action is check_flag, flag, on_present, and on_absent are all required.
  • on_present must be consume or keep. on_absent must be skip or violate.
  • Triggers with action: set_flag or action: check_flag must not declare escalation-dependent rendering (nudge/warning templates are ignored for these actions — they produce no output except via on_absent: violate, which routes through the normal evaluate rendering path).
  • Escalation after values must be non-decreasing when sorted: each successive entry must have after >= previous after. Levels must be non-decreasing in severity.
  • nudge_template length must be <= 120 chars.
  • warning_template length must be <= 500 chars.
  • block_reason length must be <= 200 chars.
  • Every id listed in behaviors/index.yaml must have a file at behaviors/<id>/behavior.yaml.

7. Complete Example: search-first

schema_version: "1"
id: search-first
name: Search Before Writing
description: >
  Require the agent to search existing code before writing new implementations.
  Prevents duplicate code and enforces codebase familiarity before modification.
category: core
scope: session
enabled: true

policy:
  triggers:
    # 1. Any search-like tool sets the flag — no violation, no counter.
    - event: PreToolUse
      matcher: "Grep|Glob|Read"
      action: set_flag
      flag: search_context_ready

    # 2. A write on a source file checks the flag.
    #    Present → consume and pass. Absent → violate (counter + escalation).
    - event: PreToolUse
      matcher: "Write|Edit"
      conditions:
        - field: file_path
          operator: regex_match
          value: '\.(py|ts|js|tsx|jsx|swift|go|rs|java|kt|rb|php|cs)$'
      logic: all
      action: check_flag
      flag: search_context_ready
      on_present: consume
      on_absent: violate

  enforcement:
    default_level: silent
    escalation:
      - after: 1
        level: nudge
      - after: 3
        level: warning
      - after: 5
        level: soft_block

  recovery:
    hint: "Use Grep or Glob to search for existing patterns before writing new code."
    suggested_tool: Grep
    suggested_action: "grep -r '<pattern>' src/"

rendering:
  nudge_template: "{behavior_name}: Consider searching first (violation {counter}/{threshold})"
  warning_template: |
    **[{behavior_id}]** You have written code {counter} times without searching first.
    Expected: use Grep/Glob to find existing patterns before implementing.
    Action: search for related code, then proceed.
    Next violation ({threshold}) triggers a block.
  block_reason: "Must search the codebase before writing new code."
  override_prompt: "Run Grep or Glob first, then retry the write operation."

applies_to:
  tools: []
  agents: []
  profiles: [standard, strict]

metadata:
  author: dotforge
  version: "1.0.0"
  tags: [search, quality, core]

8. Complete Example: no-destructive-git

schema_version: "1"
id: no-destructive-git
name: No Destructive Git Operations
description: >
  Block force pushes, hard resets, and other destructive git operations permanently.
  Safety-critical — no override available.
category: core
scope: session
enabled: true

policy:
  triggers:
    - event: PreToolUse
      matcher: "Bash"
      conditions:
        - field: command
          operator: regex_match
          value: 'git\s+(push\s+.*--force|push\s+.*-f\b|reset\s+--hard|clean\s+-f|branch\s+-[Dd])'
      logic: all

  enforcement:
    default_level: hard_block

  recovery:
    hint: "Destructive git operations are permanently blocked. Use safe alternatives: git revert, git stash, git reset --soft."
    suggested_tool: Bash
    suggested_action: "git revert HEAD  # or git stash"

rendering:
  block_reason: "Force push, hard reset, and destructive git operations are permanently blocked."

applies_to:
  tools: [Bash]
  agents: []
  profiles: [minimal, standard, strict]

metadata:
  author: dotforge
  version: "1.0.0"
  tags: [git, safety, core]

Note: no escalation defined — hard_block is the immediate and permanent level. No nudge_template or warning_template needed since the behavior never produces those levels. See SPEC.md Section 5.6 for hard_block output protocol.


9. Anti-patterns

Do NOT include these in a behavior file:

  • Runtime expressions or scripting. Conditions must use the closed DSL. No embedded bash, jq filters, or eval expressions.
  • File I/O or network calls. Behaviors are declarative. No reading external files, no HTTP calls, no database queries.
  • Cross-behavior references. A behavior cannot reference another behavior's counter, state, or output.
  • External state beyond tool_input and session_state.counter. Environment variables, filesystem state, git state, and time-based conditions are out of scope for 3.0.
  • Rendering templates that exceed length limits. nudge > 120 chars, warning > 500 chars, block_reason > 200 chars all fail validation.
  • Mixed concerns in one behavior. A behavior enforcing both search-first and test-before-done is two behaviors. Split them — each behavior must have a single, named concern.
  • Bare enabled: false as a permanence signal. Use category: experimental to signal instability; enabled: false in index.yaml for user opt-in behaviors.
  • Using scope: task or scope: project. Reserved for future versions. Set scope: session in 3.0; other values parse but produce no functional behavior.