Fleet rules
Write YAML rules that flag risky package and AI config changes across your Mac fleet.
Fleet rules tell Koban what to flag when something changes on a developer Mac, or when an item is already present in inventory. You write them in YAML. Koban compares each Mac's inventory against your rules and creates a finding when a rule matches.
Koban is a sensor, not a gate. Rules never block installs, remove packages, or edit config files. They only produce reports you can review or route to alerts.
Key terms
| Term | Meaning |
|---|---|
| Agent | The Koban app running on each Mac. It watches disk, builds an inventory, and evaluates rules. |
| Fleet | The hosted control plane where you publish rules and view synced findings across enrolled Macs. |
| Inventory | A list of what Koban found on a Mac: installed Homebrew packages, Claude MCP servers, and similar. |
| Snapshot | A point-in-time inventory. The agent saves snapshots and compares them to detect changes. |
| Surface | A category of inventory, such as Homebrew packages (homebrew) or Codex config (codexConfig). |
| Finding | A report that a rule matched. Includes what changed, why it matched, and where it lives on disk. |
| Rule | One policy in your YAML file. Defines what change to look for and how serious it is. |
What Koban watches today
| Surface | What gets inventoried | Example items |
|---|---|---|
homebrew | Formulae and casks from Homebrew | ripgrep, docker, google-chrome |
claudeConfig | Claude MCP servers, settings, agents, commands, hooks, skills, plugins, and instructions | A server named filesystem, a hook, or a slash command |
codexConfig | Codex profiles, MCP servers, hooks, rules, and skills | A profile or MCP server |
piConfig | Pi settings, shared MCP files, imports, and package metadata | An imported package or MCP server |
cursorConfig | Cursor global and project MCP files, rules, legacy rules, and instructions | A .cursor/rules entry |
opencodeConfig | OpenCode global and project config, MCP, agents, commands, plugins, and instructions | An OpenCode command or plugin |
javascriptPackages | npm, pnpm, Yarn, and Bun lockfiles | A resolved npm package |
pythonPackages | uv, pyproject, pylock, requirements, and constraints metadata | A declared or locked Python package |
Each item has metadata Koban reads from disk (package name, version, install source, command line, etc.). Rules inspect that metadata. They do not run shell commands or read arbitrary files.
How a rule runs
The agent periodically snapshots inventory and diffs it against the previous snapshot. When something is added, changed, or removed, Koban checks change-triggered rules. Rules with the present trigger also run against the current inventory snapshot, which is useful for package denylist and IOC rules that should catch software already installed before the rule was published.
A rule matches when all of these are true:
- The rule is turned on (
enabled: true). - The change happened on the same surface the rule targets (e.g. Homebrew).
- The rule's triggers include the current evaluation mode (
added,modified,removed, orpresent). - The item passes the rule's match condition (e.g. command contains
curl).
When all four pass, Koban creates a finding with a title, severity, and the file path where the item was found.
previous snapshot → current snapshot → diff/current inventory → check rules → findingWhere rules live
You can define rules in two places. Both use the same format.
On each Mac (local config)
Path: ~/.config/koban/koban.yaml
Created automatically on first launch. Koban never overwrites an existing file. Edit it and restart the agent to test rules on one machine.
In Fleet (org-wide config)
Published as a configuration bundle from the Fleet dashboard or API. Every enrolled Mac pulls the latest bundle. Fleet validates the file before agents receive it.
Users author this bundle as YAML. Fleet validates the same closed vocabulary, canonicalizes it to JSON, and serves that JSON to enrolled agents over the sensor protocol.
Local config can also set machine-specific paths (which Homebrew install to scan, which Claude config file to read). Fleet bundles should stay focused on shared policy, timing, enabled surfaces, and sync limits.
Fleet delivers remote configuration over authenticated sensor routes. See Authentication for enrollment tokens, device certificates, and the mTLS boundary.
Configuration file structure
A Fleet config file has top-level sections. You can omit any section; Koban fills in defaults.
| Section | What it controls |
|---|---|
generation | Fleet only. A version label (e.g. a date or number). Change it whenever you update policy so Macs know to download the new file. |
watch | How often the agent rechecks disk after a change is detected. |
homebrew, claude, codex, pi, cursor, opencode, javascript, python | Whether to inventory each surface and which optional categories are enabled. |
sync | Fleet only. How much data an agent sends per upload. |
rules | Your policy list. This is where most of the file lives. |
Watch timing
watch:
debounceMilliseconds: 800 # wait this long after a file change before rescanning
pollIntervalSeconds: 300 # full rescan every 5 minutes as a backupDefaults: 800 ms debounce, 300 s poll interval.
Surface toggles
homebrew:
enabled: true
prefixes: null # local agent only: which Homebrew dirs to scan (null = auto-detect)
claude:
enabled: true
configPath: null # local agent only: path to Claude config (null = ~/.claude.json)Sync limits (Fleet)
sync:
maxBatchBytes: 524288 # max upload size per request (512 KB)
maxBatchEvents: 500 # max events per requestDefault rules
If you omit the rules section entirely, Koban ships a built-in rule set (new MCP servers, suspicious commands, new hooks and skills, new packages, third-party Homebrew taps, etc.). See Built-in rules.
Full default file: koban.default.yaml
Writing a rule
Each entry under rules: is one policy. Here is a minimal rule that flags any new MCP server:
rules:
- id: team.mcp.new-server
surface: claudeConfig
triggers: [added]
match: always
severity: notable
title: New MCP server
rationale: A new MCP server was added to Claude configuration.Required fields
| Field | What to put |
|---|---|
id | A unique name for this rule. Use dots for grouping, e.g. homebrew.untrusted-tap. Appears in findings and alerts. |
surface | One of the v1 surfaces listed above. The rule only applies to changes on this surface. |
match | How to decide if an item matches. See Match types. |
severity | How urgent the finding is: info, notable, suspicious, or critical. |
title | Short headline shown in the UI and alerts. |
rationale | One or two sentences explaining why this matters to a reviewer. |
Optional fields
| Field | Default | What to put |
|---|---|---|
enabled | true | Set to false to turn off a rule without deleting it. |
triggers | [added, modified] | Which evaluation modes fire the rule. See Triggers. |
Depending on the match type, you also need one or more of: field, values, allowed, schemes, flag, expected.
Full example
generation: "2026-05-30"
watch:
debounceMilliseconds: 800
pollIntervalSeconds: 300
homebrew:
enabled: true
claude:
enabled: true
sync:
maxBatchBytes: 524288
maxBatchEvents: 500
rules:
- id: team.mcp.new-server
surface: claudeConfig
triggers: [added]
match: always
severity: notable
title: New MCP server
rationale: A new MCP server was added to Claude configuration.
- id: team.mcp.suspicious-command
surface: claudeConfig
triggers: [added, modified]
match: fieldContainsAny
field: detail
values: [curl, wget, "| sh", "| bash", "bash -c", "sh -c", eval, base64]
severity: suspicious
title: Suspicious command
rationale: Command contains shell patterns used to download and run remote code.
- id: homebrew.untrusted-tap
surface: homebrew
triggers: [added, modified, present]
match: fieldNotInList
field: origin
allowed: [homebrew/core, homebrew/cask]
severity: notable
title: Third-party tap
rationale: Package came from a Homebrew tap outside the official core and cask repos.
- id: homebrew.ioc.compromised-copilot-for-xcode
surface: homebrew
triggers: [added, modified, present]
match: fieldContainsAny
field: name
values: [copilot-for-xcode]
severity: critical
title: Compromised Homebrew package
rationale: This package matches a published compromise indicator and should be reviewed.
- id: homebrew.unrequested-install
surface: homebrew
triggers: [added]
match: flagEquals
flag: installedOnRequest
expected: false
severity: info
title: Pulled in as a dependency
rationale: Package was installed automatically as a dependency, not requested by the user.Fields you can match on
Rules inspect normalized metadata on each inventory item. You cannot match on raw file contents or custom paths. The vocabulary is a closed set: rules may only look at the fields and flags below, which keeps the engine bounded.
String fields
Used by fieldContainsAny, fieldNotInList, and fieldHasURLScheme. A field that is empty or not applicable to the item's surface is treated as missing (the rule does not match).
| Field | Available on | What it contains |
|---|---|---|
kind | All surfaces | Item kind, e.g. mcpServer, hook, skill, command, package. |
name | All surfaces | Package or MCP server name. |
version | Homebrew, packages | Installed or resolved version string. |
origin | Homebrew, packages | Source the item came from (Homebrew tap, or the package manager for JS/Python). |
detail | Agent configs, packages | Full command line, remote URL, or provenance detail for the item. |
path | All surfaces | On-disk path where Koban found the item. |
packageManager | JS/Python packages | The package manager that produced the item (npm, pnpm, yarn, bun, uv, pip). |
registry | JS/Python packages | Host of the resolved download URL, e.g. registry.npmjs.org. |
sourceURL | JS/Python packages | Resolved download URL for the package. |
command | Agent MCP | Local launch command for an MCP server (empty when the server is remote). |
dependencyScope | JS/Python packages | Declared dependency scope (e.g. dev vs. direct), when present. |
fileHash | Where available | Content hash of the item, when the collector records one. |
Boolean flags
Used by flagEquals. A flag that the item's surface has no concept of is treated as missing.
| Flag | What it means |
|---|---|
installedOnRequest | true if the user installed the item directly; false if pulled in as a dependency. |
hasInstallScript | Package declares an install/postinstall script. |
isDirectDependency | Item is a direct dependency rather than transitive. |
isEditable | Package is installed in editable/develop mode. |
isExecutableConfig | Config item can execute code (hook, command, plugin, MCP server). |
isPromptShapingConfig | Config item shapes prompts/behavior (agent, instruction, rule, setting, skill). |
usesDynamicAuthHelper | MCP server resolves credentials through a dynamic auth helper. |
usesEphemeralRunner | Item launches through an ephemeral runner (npx, uvx, bunx, pnpm dlx). |
usesRemote | Item connects to a remote endpoint (http, https, ws, wss). |
Findings also record the on-disk path where Koban found the item, even if the rule matched a different field.
Triggers
Triggers define when a rule can fire. Change triggers use the diff between snapshots. The present trigger uses the current snapshot itself.
| Trigger | When it fires | Example |
|---|---|---|
added | Item did not exist before | New MCP server added to Claude config |
modified | Item existed but something changed | MCP command updated, package upgraded |
removed | Item no longer present | MCP server deleted |
present | Item exists in the current inventory snapshot | Known-compromised package already installed |
If you leave triggers out, Koban uses [added, modified].
Match types
The match field picks the condition to test. Koban supports exactly five types. There is no regex, semver comparison, or custom scripting.
always
Match every item that hits the surface and trigger. No extra fields needed.
Use when: you want to know about any new item, such as every new MCP server.
match: alwaysfieldContainsAny
Match when a text field contains any of the listed substrings. Case-insensitive.
Use when: you want to catch dangerous command patterns or package runners.
match: fieldContainsAny
field: detail
values: [npx, uvx, bunx, "pnpm dlx"]Does not match if the field is empty or missing.
fieldNotInList
Match when a text field has a value not in your allow list. The field must be non-empty.
Use when: you want to allow only official Homebrew taps and flag everything else.
match: fieldNotInList
field: origin
allowed: [homebrew/core, homebrew/cask]fieldHasURLScheme
Match when a text field is a URL starting with one of the listed schemes. Case-insensitive.
Use when: you want to flag MCP servers that connect to a remote host instead of running locally.
match: fieldHasURLScheme
field: detail
schemes: [http, https, ws, wss]flagEquals
Match when a boolean flag equals true or false. See Boolean flags for the full set, e.g. installedOnRequest, usesEphemeralRunner, or isExecutableConfig.
Use when: you want to list packages installed as dependencies, or flag config items that can execute code.
match: flagEquals
flag: installedOnRequest
expected: falseDoes not match if the flag is not available (e.g. on Claude MCP items).
Severity
Severity tells reviewers and alert routing how to prioritize a finding.
| Severity | Priority | Typical examples |
|---|---|---|
info | Low | Dependency pulled in by another install |
notable | Medium | New MCP server, third-party Homebrew tap, remote MCP endpoint |
suspicious | High | Command that downloads and executes remote code |
critical | Highest | Known compromised package or confirmed IOC |
Severities rank in order: info, then notable, then suspicious, then critical. On a Mac running the agent, the menu bar icon reflects the highest open severity.
Built-in rules
If you do not define rules, Koban ships a built-in default set. The same agent-config rules apply across every agent surface (claudeConfig, codexConfig, piConfig, cursorConfig, opencodeConfig), so the rule IDs below are shared rather than per-tool.
MCP servers
| Rule ID | What it catches | Severity |
|---|---|---|
agent.mcp.new-server | Any new MCP server added | notable |
agent.mcp.dynamic-auth-helper | MCP server that resolves credentials through a dynamic auth helper | notable |
Agent configuration
| Rule ID | What it catches | Severity |
|---|---|---|
agent.config.ephemeral-runner | Config launched via npx, uvx, bunx, or pnpm dlx | notable |
agent.config.suspicious-command | Command contains curl, wget, shell pipes, eval, or base64 | suspicious |
agent.config.remote-transport | Config points at an http, https, ws, or wss endpoint | notable |
agent.config.new-hook | New hook added | notable |
agent.config.new-skill | New skill added | notable |
agent.config.new-plugin | New plugin added | notable |
agent.config.new-command | New command added | notable |
agent.config.new-rule | New rule added | notable |
agent.config.new-instruction | New instruction file added | notable |
agent.config.new-settings | New setting added | notable |
Packages
| Rule ID | What it catches | Severity |
|---|---|---|
packages.new-javascript-package | New JavaScript package resolved in a lockfile | info |
packages.new-python-package | New Python package declared or locked | info |
packages.known-malicious-javascript | JavaScript package name matching a public malicious-package campaign | suspicious |
packages.known-malicious-python | Python package name matching a public malicious-package report | suspicious |
packages.javascript.untrusted-registry | JavaScript package resolved from a non-default registry | notable |
Homebrew
| Rule ID | What it catches | Severity |
|---|---|---|
homebrew.untrusted-tap | Package from a tap other than homebrew/core or homebrew/cask | notable |
homebrew.unrequested-install | Package installed as a dependency, not directly | info |
To customize policy, replace the entire rules list. Copy the defaults from koban.default.yaml first if you only want to change one rule.
What a finding contains
When a rule matches, Koban records:
| Field | What you see |
|---|---|
ruleId | Which rule matched |
title | Headline from the rule |
rationale | Explanation from the rule |
severity | info, notable, suspicious, or critical |
itemName | Name of the package or MCP server |
evidence.path | File path on the Mac |
evidence.detail | Full command line or URL (when available) |
evidence.matchedField | Which field triggered the match (empty for always) |
evidence.matchedValue | The value that triggered the match |
Publishing rules to Fleet
- Write or edit your config file in YAML (JSON also works with the same field names).
- Set a new
generationvalue every time you change policy. - Publish through Fleet. Invalid rules are rejected before any Mac downloads them.
- Fleet stores the validated bundle as canonical JSON.
- Enrolled agents fetch the JSON bundle over authenticated sensor routes, validate it locally, and apply it.
Fleet rejects a config if:
generationis missing- Watch or sync numbers are zero or negative
- Any rule is missing
id,title, orrationale - Any rule uses an unknown surface, severity, trigger, or match type
- A match type is missing its required extra fields (e.g.
fieldContainsAnywithoutvalues)
Testing on one Mac
Before publishing org-wide:
- Edit
~/.config/koban/koban.yamlon a test Mac. - Restart the Koban agent.
- Make a change that should trigger your rule, or use
presentand wait for the next scan to evaluate current inventory. - Open the agent and confirm the finding appears with the expected title and severity.
The agent is created on first launch and never overwritten, so your edits are safe.
What rules cannot do
Koban rules are intentionally limited:
- Cannot block, quarantine, or uninstall anything
- Cannot run shell commands on the Mac
- Cannot scan arbitrary directories or file contents
- Cannot compare semver ranges (e.g. "flag if version > 2.0")
- Cannot use regular expressions
- Cannot reference other rules or external data sources
Reading package manager formats (Homebrew receipts, Claude JSON) happens in agent code. Rules only see the structured inventory fields listed above.
Alerting
When findings sync to Fleet, each one includes the Mac identifier, rule ID, severity, and file path. Connect Slack, webhooks, or your SIEM from Fleet settings to route alerts to your team.