General preferences (built-in defaults)
General preferences (built-in defaults)
multiagent-protocol ships with a set of opinions that apply by default to every installation. They are encoded as built-in skills with severity = P0 (blocking) or as classifier rules that are always loaded.
These are the things the protocol believes are true for every solo developer using AI agents, regardless of their specific stack. If you disagree, you can override per-rule in config/skills.yml, but the override is opt-out — the defaults assume you want them.
This document lists each default, the rationale, and how to disable it.
1. No hallucinated references in commits
Built-in: hook_hallucination_guard.py (BranchHook, P0)
A commit’s message body is scanned for \
Why: AI agents frequently reference imaginary files in commit messages (“see src/auth/legacy.py” when no such file exists, or never existed). These references survive into the project’s history and confuse later agents (and humans) who go looking.
How to disable: in config/skills.yml:
disabled:
- hook_hallucination_guard
Disabling is appropriate for prototype repos or repos with intentionally-placeholder paths. For a project intended to outlive a few weeks, keep it enabled.
2. No personal data in source code
Built-in: .github/workflows/tests.yml job no-personal-data (runs CI scan via scripts/scan_no_personal_data.py)
Source files under src/, tests/, schemas/, .github/workflows/, top-level *.py|*.yml|*.toml are scanned for:
- Email addresses (
@example.comand@example.orgare allowed; other domains fail). - Public IPv4 addresses (private ranges allowed).
- SSH-style
Host <alias>literals.
Why: Personal data leaks through example code more often than people realize. A real GitHub login or email left in a fork’s example becomes a real attack surface (someone could spear-phish the address, or use the handle to build a believable social-engineering pretext) once the fork is public. The protocol refuses to ship its own embarrassment.
How to disable: edit .github/workflows/tests.yml to drop the no-personal-data job. The scan script (scripts/scan_no_personal_data.py) is itself a regular file; you can also edit its patterns. But the default is “on” for every fresh fork.
3. Agent-* commit trailers required
Built-in: validator_trailers.py (Validator, P0)
Every commit in a PR must have these trailers, well-formed:
Agent-Tool: <one of agent_registry.tools>Agent-Model: <model id or n/a>Agent-Session: s_[a-z0-9-]{2,14}[a-z0-9]Agent-Machine: <handle>Task-Ref: <Issue#N|PR#N|none|round-X/topic|bot/topic>
Why: Without these, you cannot tell which agent / model / session / machine authored a commit. When two agents step on each other, you need to be able to read git log and reconstruct who did what. This is the most basic forensic capability; the protocol treats it as non-negotiable.
How to disable: not permitted via disabled:. You can lower severity via severity_overrides: validator_trailers: P2 (warn but don’t block), but this is strongly discouraged — agents that don’t write trailers will not write them voluntarily once the gate stops failing.
4. Empty PR is Quadrant D
Built-in: classifier_empty_pr.py (ClassifierRule)
A PR with zero file changes votes Quadrant D in the classifier.
Why: An empty PR with ready-to-merge is suspicious — either a bot bug, a race condition, or an attacker probing your gate. Forcing owner review on empty PRs is a cheap defense.
How to disable:
disabled:
- classifier_empty_pr
Disabling is appropriate if you have a workflow that creates intentionally-empty marker PRs for releases. Most operators do not.
5. Bot’s own repo PRs are Quadrant D
Built-in: classifier_bot_self_repo.py (ClassifierRule)
A PR whose target repository equals the bot’s own repository votes Quadrant D.
Why: The bot cannot gate its own PRs without chicken-and-egg. Forcing them to Quadrant D ensures the owner sees them — the only path to merge is [break-glass-bot-self-update] direct push + ADR-within-24h, or owner-approved manual merge via the App’s owner-deploy credential (planned).
How to disable: not permitted. This is structural.
6. classifier-judgment must be published by a canonical actor
Built-in: validator_classifier_publisher.py (Validator, P0) +
classifier_published_verdict.py (ClassifierRule)
When the bot reads a classifier-judgment check-run from a PR, it verifies app.slug == config/env.yml classifier_publisher_slug (default: github-actions). Mismatched slug → fail-closed (treated as classifier missing → Quadrant D default).
As of v1.1, the bot also reads the published quadrant from a canonical classifier-judgment check-run and votes it (rule classifier_published_verdict): the check-run’s output/summary should contain a line Quadrant: X (one of A/B/C/D). Because the classifier takes the maximum quadrant, the published verdict can only raise a PR’s quadrant toward owner control — never lower it. A check-run that is absent, by a non-canonical publisher, or unparseable is ignored (the rule abstains).
Why: Without the publisher gate, anyone who can run a GitHub Actions workflow in any repo could publish a check-run named classifier-judgment with summary Quadrant: A. The bot’s auto-approval path would honor it. The publisher-identity gate closes this attack — and because the published-verdict rule honours only the canonical slug and can only raise, a forged verdict can neither unlock a flagged PR nor (since a raise is at worst a denial-of-service) silently auto-merge.
How to disable: the publisher validator is not permitted to be disabled. Change config/env.yml classifier_publisher_slug if you publish classifier-judgment from a different App (advanced); the gate itself stays on. The published-verdict rule abstains automatically when no canonical judgment is present, so a fleet that does not publish classifier-judgment is unaffected.
7. Mirror cascade is detection, not auto-fix
Built-in: drift_check.py (module)
When the bot detects that a canonical path in a supervised repo diverges from the governance repo, it opens an Issue (decision:mirror-drift-incident). It does not automatically open a cascade PR.
Why: Auto-cascade is itself a Quadrant D operation (it modifies critical files in adopters without owner approval at the time). Until ADR R-N+1 explicitly authorizes auto-cascade, detection-only is the safe default.
How to enable auto-cascade (when implemented): a future version will add an explicit setting to config/projects.yml (likely under a drift_check: block) that turns auto-cascade on. The exact key name will be defined in the ADR that authorizes the bot to open critical-path PRs in adopters; until then, no schema key exists for this. Not in v1.0.
8. Break-glass requires ADR within 24 hours
Built-in: hook_break_glass_audit.py (BranchHook, P0)
A commit on main whose subject starts with [break-glass-*] triggers L5 audit: actor allowlist check + ADR-within-24h existence check. Missing ADR opens decision:break-glass-unaudited Issue.
Why: Break-glass is intentionally costly. The ADR documents what you did and why — the cost discourages casual use, the audit trail keeps the project’s reasoning intact across time. See docs/concepts/break-glass.md.
How to disable: not permitted via disabled:. You can extend the 24h window via config/projects.yml break_glass.adr_deadline_hours (default 24), but the audit hook always runs.
9. Decision Inbox issues track owner reactions only
Built-in: decision_inbox.py only counts reactions/comments from actors in config/owner.yml allowlisted_actors.
Why: Without this, a teammate or an attacker could approve Quadrant D PRs the owner did not see. The allowlist ensures only authorized actors can route around the owner.
How to extend: add additional GitHub logins to config/owner.yml allowlisted_actors. The list is authoritative; no implicit additions.
10. No network calls from user skills
Built-in: skills loader rejects user skills that import requests, import urllib, import socket, etc.
Why: Skills run with full bot privileges. A skill making an external API call is a data exfiltration vector — the operator’s GitHub token, PR content, and bot state are all in-process. By making the rule “no network from user skills,” skills become pure functions of context.
How to extend with network: see docs/guide/skills.md § “When you cannot do it with a skill”. The right answer is to extend PRContext (a core change with ADR) rather than punch network through a skill.
11. Unauthorized pushes to main are flagged (code-level branch protection)
Built-in: hook_unauthorized_push.py (BranchHook) — added v1.1
GitHub’s “only the App may push to main” branch protection is a paid feature on private repos. This hook is the code-level substitute: scanning main, it opens a decision:unauthorized-push incident for any commit that is not a sanctioned write — i.e. its committer is not the bot, its subject is not a [break-glass-*] commit (those belong to the break-glass auditor, § 8), and its committer login is not in config/owner.yml allowlisted_actors.
Why: Without paid branch protection, nothing at GitHub’s layer stops a stray push or a leaked token from writing to main around the merge gate. This hook turns such a write into a visible incident on the next tick.
How to disable: not permitted via disabled:. Because this hook is the only code-level substitute for paid branch protection, allowing it to be silently turned off would leave a fleet with nothing watching main for unsanctioned writes — a fail-open. It is in the non-disableable core set (alongside validator_trailers, validator_classifier_publisher, classifier_bot_self_repo, and hook_break_glass_audit). If you already enforce bot-only pushes at GitHub’s layer (paid branch protection / a ruleset), the hook is simply redundant — its incidents will never fire, at no cost — so there is no need to disable it.
Note on identity. This hook (and the break-glass auditor, § 8) authorizes by the commit’s committer login, not the author — the author field is trivially forgeable via
git commit --author=.... Committer metadata is itself an association (the GitHub API matches the committer email to an account) rather than the true push actor; the authoritative push actor is only available via the Enterprise-tier audit-log API. Wiring that in is a documented future item.
Severity override table
For any built-in skill, you may override severity in config/skills.yml:
severity_overrides:
validator_trailers: P2 # Warn, do not block. Strongly discouraged.
hook_hallucination_guard: P3 # Audit only, no Issue opened.
no_wip_markers: P0 # Promote a user skill from default P1 to P0.
Severity levels:
P0— block immediately. Failing check fails the L1 gate.P1— block after 60-day burn-in window. New agents have a grace period.P2— warn only. Bot comments but the gate still passes.P3— audit only. Bot records in metrics but does not comment.
Lowering a built-in below its default severity is the operator’s call; the protocol does not refuse, but the audit log records the override on every tick so the reasoning is auditable.
How to add a general preference
If you believe a new opinion should be a built-in default for every installation:
- Open an Issue tagged
proposal: general-preference. - Argue: (a) every solo-dev installation will want this, (b) the cost of false-positive is low, (c) the cost of false-negative is high.
- If consensus forms, write a Quadrant D PR adding the new built-in. The PR description must list the rationale; the ADR (filed in the same PR or as a follow-up) becomes part of
docs/decisions/.
The bar for new general preferences is high. The current list is the result of one project’s specific mistakes; new entries should be the result of new specific mistakes, not abstract concerns.