Koban

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

TermMeaning
AgentThe Koban app running on each Mac. It watches disk, builds an inventory, and evaluates rules.
FleetThe hosted control plane where you publish rules and view synced findings across enrolled Macs.
InventoryA list of what Koban found on a Mac: installed Homebrew packages, Claude MCP servers, and similar.
SnapshotA point-in-time inventory. The agent saves snapshots and compares them to detect changes.
SurfaceA category of inventory, such as Homebrew packages (homebrew) or Codex config (codexConfig).
FindingA report that a rule matched. Includes what changed, why it matched, and where it lives on disk.
RuleOne policy in your YAML file. Defines what change to look for and how serious it is.

What Koban watches today

SurfaceWhat gets inventoriedExample items
homebrewFormulae and casks from Homebrewripgrep, docker, google-chrome
claudeConfigClaude MCP servers, settings, agents, commands, hooks, skills, plugins, and instructionsA server named filesystem, a hook, or a slash command
codexConfigCodex profiles, MCP servers, hooks, rules, and skillsA profile or MCP server
piConfigPi settings, shared MCP files, imports, and package metadataAn imported package or MCP server
cursorConfigCursor global and project MCP files, rules, legacy rules, and instructionsA .cursor/rules entry
opencodeConfigOpenCode global and project config, MCP, agents, commands, plugins, and instructionsAn OpenCode command or plugin
javascriptPackagesnpm, pnpm, Yarn, and Bun lockfilesA resolved npm package
pythonPackagesuv, pyproject, pylock, requirements, and constraints metadataA 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:

  1. The rule is turned on (enabled: true).
  2. The change happened on the same surface the rule targets (e.g. Homebrew).
  3. The rule's triggers include the current evaluation mode (added, modified, removed, or present).
  4. 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  →  finding

Where 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.

SectionWhat it controls
generationFleet only. A version label (e.g. a date or number). Change it whenever you update policy so Macs know to download the new file.
watchHow often the agent rechecks disk after a change is detected.
homebrew, claude, codex, pi, cursor, opencode, javascript, pythonWhether to inventory each surface and which optional categories are enabled.
syncFleet only. How much data an agent sends per upload.
rulesYour 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 backup

Defaults: 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 request

Default 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

FieldWhat to put
idA unique name for this rule. Use dots for grouping, e.g. homebrew.untrusted-tap. Appears in findings and alerts.
surfaceOne of the v1 surfaces listed above. The rule only applies to changes on this surface.
matchHow to decide if an item matches. See Match types.
severityHow urgent the finding is: info, notable, suspicious, or critical.
titleShort headline shown in the UI and alerts.
rationaleOne or two sentences explaining why this matters to a reviewer.

Optional fields

FieldDefaultWhat to put
enabledtrueSet 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).

FieldAvailable onWhat it contains
kindAll surfacesItem kind, e.g. mcpServer, hook, skill, command, package.
nameAll surfacesPackage or MCP server name.
versionHomebrew, packagesInstalled or resolved version string.
originHomebrew, packagesSource the item came from (Homebrew tap, or the package manager for JS/Python).
detailAgent configs, packagesFull command line, remote URL, or provenance detail for the item.
pathAll surfacesOn-disk path where Koban found the item.
packageManagerJS/Python packagesThe package manager that produced the item (npm, pnpm, yarn, bun, uv, pip).
registryJS/Python packagesHost of the resolved download URL, e.g. registry.npmjs.org.
sourceURLJS/Python packagesResolved download URL for the package.
commandAgent MCPLocal launch command for an MCP server (empty when the server is remote).
dependencyScopeJS/Python packagesDeclared dependency scope (e.g. dev vs. direct), when present.
fileHashWhere availableContent 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.

FlagWhat it means
installedOnRequesttrue if the user installed the item directly; false if pulled in as a dependency.
hasInstallScriptPackage declares an install/postinstall script.
isDirectDependencyItem is a direct dependency rather than transitive.
isEditablePackage is installed in editable/develop mode.
isExecutableConfigConfig item can execute code (hook, command, plugin, MCP server).
isPromptShapingConfigConfig item shapes prompts/behavior (agent, instruction, rule, setting, skill).
usesDynamicAuthHelperMCP server resolves credentials through a dynamic auth helper.
usesEphemeralRunnerItem launches through an ephemeral runner (npx, uvx, bunx, pnpm dlx).
usesRemoteItem 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.

TriggerWhen it firesExample
addedItem did not exist beforeNew MCP server added to Claude config
modifiedItem existed but something changedMCP command updated, package upgraded
removedItem no longer presentMCP server deleted
presentItem exists in the current inventory snapshotKnown-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: always

fieldContainsAny

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: false

Does 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.

SeverityPriorityTypical examples
infoLowDependency pulled in by another install
notableMediumNew MCP server, third-party Homebrew tap, remote MCP endpoint
suspiciousHighCommand that downloads and executes remote code
criticalHighestKnown 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 IDWhat it catchesSeverity
agent.mcp.new-serverAny new MCP server addednotable
agent.mcp.dynamic-auth-helperMCP server that resolves credentials through a dynamic auth helpernotable

Agent configuration

Rule IDWhat it catchesSeverity
agent.config.ephemeral-runnerConfig launched via npx, uvx, bunx, or pnpm dlxnotable
agent.config.suspicious-commandCommand contains curl, wget, shell pipes, eval, or base64suspicious
agent.config.remote-transportConfig points at an http, https, ws, or wss endpointnotable
agent.config.new-hookNew hook addednotable
agent.config.new-skillNew skill addednotable
agent.config.new-pluginNew plugin addednotable
agent.config.new-commandNew command addednotable
agent.config.new-ruleNew rule addednotable
agent.config.new-instructionNew instruction file addednotable
agent.config.new-settingsNew setting addednotable

Packages

Rule IDWhat it catchesSeverity
packages.new-javascript-packageNew JavaScript package resolved in a lockfileinfo
packages.new-python-packageNew Python package declared or lockedinfo
packages.known-malicious-javascriptJavaScript package name matching a public malicious-package campaignsuspicious
packages.known-malicious-pythonPython package name matching a public malicious-package reportsuspicious
packages.javascript.untrusted-registryJavaScript package resolved from a non-default registrynotable

Homebrew

Rule IDWhat it catchesSeverity
homebrew.untrusted-tapPackage from a tap other than homebrew/core or homebrew/casknotable
homebrew.unrequested-installPackage installed as a dependency, not directlyinfo

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:

FieldWhat you see
ruleIdWhich rule matched
titleHeadline from the rule
rationaleExplanation from the rule
severityinfo, notable, suspicious, or critical
itemNameName of the package or MCP server
evidence.pathFile path on the Mac
evidence.detailFull command line or URL (when available)
evidence.matchedFieldWhich field triggered the match (empty for always)
evidence.matchedValueThe value that triggered the match

Publishing rules to Fleet

  1. Write or edit your config file in YAML (JSON also works with the same field names).
  2. Set a new generation value every time you change policy.
  3. Publish through Fleet. Invalid rules are rejected before any Mac downloads them.
  4. Fleet stores the validated bundle as canonical JSON.
  5. Enrolled agents fetch the JSON bundle over authenticated sensor routes, validate it locally, and apply it.

Fleet rejects a config if:

  • generation is missing
  • Watch or sync numbers are zero or negative
  • Any rule is missing id, title, or rationale
  • Any rule uses an unknown surface, severity, trigger, or match type
  • A match type is missing its required extra fields (e.g. fieldContainsAny without values)

Testing on one Mac

Before publishing org-wide:

  1. Edit ~/.config/koban/koban.yaml on a test Mac.
  2. Restart the Koban agent.
  3. Make a change that should trigger your rule, or use present and wait for the next scan to evaluate current inventory.
  4. 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.