The rules file format
Every field of rules.json and settings.json: the envelope, the rule fields, the matchers and the settings keys, as Outbound writes and reads them.
Outbound keeps its rules in rules.json and its settings in settings.json, both in the app group container (or wherever OUTBOUND_STORE_PATH points). Both files are versioned JSON, written by the app and the background agent; outboundctl reads and writes the same formats. You rarely need to edit them by hand: the Rules pane and outboundctl rules are the supported ways to change rules, and backup is the supported way to move a configuration. But every field is documented here, because the same formats are what a managed rule feed delivers and what outboundctl rules import and rules export read and write.

Where the files are
Section titled “Where the files are”~/Library/Group Containers/5GS535L4GD.com.outboundfirewall.outbound/├── rules.json├── settings.json└── review.jsonFull details, including the .bak and .lock sidecar files, are in Where Outbound keeps its data.
rules.json
Section titled “rules.json”{ "schemaVersion": 2, "ruleSet": { "rules": [ ], "blocklists": [ ] }}- The envelope carries
schemaVersion(2 for files this version writes) andruleSet. rulesholds every rule that is not compacted intoblocklists.blocklistsholds compacted entries from blocklist subscriptions: rules that name no process, go only outgoing, have any port and protocol, live forever, are enabled, are not priority, belong to no profile and have no note. Entries are grouped by subscription, action and creation date, with the hosts and domains as separate arrays. This is a size optimization; expanding the groups on load reproduces the same rules, so the two shapes are equivalent. Rules that do not fit the pattern (including addresses and networks from a list) stay inrulesas ordinary rules.configurationDegradedandconfigurationDegradedReasonare optional diagnostic fields; older decoders ignore them.
A file with "schemaVersion": 1 (an older build) is migrated automatically on load. A schema above 2 is never overwritten and is reported with “The rule store uses schema version N, which this version of Outbound does not understand.”
Dates (createdAt, modifiedAt and the date inside an until lifetime) are written as numbers counting seconds since the 2001-01-01 reference date, so a round trip is lossless. When reading, ISO-8601 strings (with or without fractional seconds) are also accepted. modifiedAt is omitted when it equals createdAt.
The rule object
Section titled “The rule object”| Field | Type | Meaning |
|---|---|---|
id |
UUID | Identifies the rule. Unique in the store: a save with a repeated id is refused. |
action |
String | allow, deny or ask. |
direction |
String | outgoing, incoming or any. |
process |
Matcher | Which program the rule is for. See below. |
via |
Matcher, optional | An intermediate program (a browser, a helper). Absent or {"any":{}} means no via condition. |
remote |
Matcher | Which destination. See below. |
ports |
Matcher | {"any":{}} or {"ports":{"_0":[[80,80],[8000,8100]]}} with ranges of port numbers 0 to 65535. |
proto |
String | tcp, udp, icmp or any. |
lifetime |
Matcher | How long the rule lives. See below. |
priority |
Boolean | Priority rules are compared before everything else. |
enabled |
Boolean | A disabled rule is kept but never matches. |
profileID |
UUID, optional | The profile the rule belongs to; nil applies in every profile. |
source |
Matcher | Who owns the rule: user, managed, ruleGroup or blocklist. See below. |
createdAt |
Date | When the rule was created. |
modifiedAt |
Date | When the rule’s meaning last changed; omitted when it equals createdAt. |
note |
String | The rule’s note; usually empty. |
processCDHash |
String, optional | Code hash the rule was approved for, narrowing a path rule. |
processPinnedPath |
String, optional | The original approved path. |
The same fields are required or optional in a managed rule feed; a feed rule’s source is replaced with managed on install whatever it says.
Matchers
Section titled “Matchers”Matchers are encoded the way Swift encodes them: one key for the case, holding an object. An unlabelled value sits under _0 (and _1, and so on, for several); a labelled value uses its own name; a nil labelled value drops out of the object.
Process matchers (which program):
| JSON | Meaning |
|---|---|
{"any":{}} |
Every program. |
{"codeIdentity":{"teamID":"ABCDE12345","signingID":"com.example.app"}} |
A signing identifier, optionally pinned to a team. teamID may be omitted. |
{"path":{"_0":"/usr/bin/curl"}} |
The path of an executable. |
{"appleSigned":{"signingIDPrefix":"com.example.prefix"}} |
Code signed by Apple, verified by the filter, whose signing identifier starts with the prefix; a nil prefix drops the signingIDPrefix key (just {"appleSigned":{}}) and means any Apple-signed code. |
{"appleSignedCode":{"_0":"system"}} |
Apple-signed code outside any .app bundle (daemons, system services). "app" means code inside an .app bundle. Used by the “Trusted by default” groups. |
Remote matchers (which destination):
| JSON | Meaning |
|---|---|
{"any":{}} |
Every destination. |
{"domain":{"_0":"example.com"}} |
The domain and all its subdomains. |
{"host":{"_0":"a.example.com"}} |
Exactly that host. |
{"address":{"_0":"203.0.113.7"}} |
One IPv4 or IPv6 address, normalized. |
{"network":{"_0":"10.0.0.0/8"}} |
A CIDR network, IPv4 or IPv6. |
{"localNetwork":{}} |
The local network ranges (10/8, 172.16/12, 192.168/16, 169.254/16, 127/8, ::1, fe80::/10, fc00::/7) plus your own extra ranges. |
Lifetimes:
| JSON | Meaning |
|---|---|
{"forever":{}} |
Never expires. |
{"untilQuit":{"pid":1234,"processStart":<date>}} |
Until the process quits (pid and start time). |
{"untilLogout":{"sessionID":"<sessionID>"}} |
Until this login session ends. |
{"untilRestart":{"bootSessionUUID":"<bootUUID>"}} |
Until this Mac restarts. |
{"until":{"_0":"2027-01-01T00:00:00Z"}} |
Until a date. |
{"schedule":{"_0":{"weekdays":[1,2,3],"start":540,"end":1020}}} |
A weekly window in local time; weekdays are 1 to 7, start and end are minutes from midnight, and an overnight window belongs to the day it starts. |
Source:
| JSON | Meaning |
|---|---|
{"user":{}} |
Your own rule. |
{"managed":{}} |
From the organization’s feed. |
{"ruleGroup":{"_0":"<uuid>"}} |
A rule group: trusted-by-default groups, accepted allow-list entries. |
{"blocklist":{"_0":"<uuid>"}} |
A rule from a blocklist subscription. |
Which rule wins when several match is decided by precedence, not by file order: see Rules.
Writing and safety
Section titled “Writing and safety”- Saves are atomic and durable, and the previous file is copied to
rules.json.bakfirst, but only when that file is itself valid, so a corrupt main file never replaces a good backup. - Writes are serialized by an advisory lock on
rules.json.lock, so the app, the background agent andoutboundctlnever lose each other’s changes. - If both the main file and the backup are unreadable, the store reports an error rather than starting empty.
outboundctl rules importis the supported way to replace an unreadable store: it starts from the imported file.
settings.json
Section titled “settings.json”{ "schemaVersion": 1, "settings": { }}- The envelope is
{"schemaVersion":1,"settings":{…}}, written pretty-printed with sorted keys, and serialized by the same kind of.lockfile. - A missing file means first launch and gets the defaults (Learn mode). A file that exists but cannot be read or decoded is an error: the app never treats that as “defaults” and overwrites it. A schema above 1 is never overwritten, with “settings.json uses schema N, which this version does not understand”.
- Managed preferences are never written here. They are read from the managed-preference domain and laid over the policy at run time. See Managed preferences.
The settings object
Section titled “The settings object”| Key | Type | Meaning |
|---|---|---|
policy |
Object | The policy pushed to the filter: mode, timeouts, pausing. See below. |
blocklists |
Array | Subscriptions: id, name, url, action (deny for a blocklist, allow for an allow list), pinnedSHA256 (optional), refreshInterval (seconds; default 86400), lastFetched, etag, peakEntries. |
profiles |
Object | profiles (each with id, name, and an optional mode that comes on with the profile) and activeID (nil = No Profile). |
trustedGroups |
Object | Which “Trusted by default” groups are on (enabled), the apps the “apps you already have” group was built from (installedAppsSnapshot), and the VPN apps the VPN group was last built from (vpnApps). |
knownNetworks |
Object | Networks this Mac has joined (networks, each with id, fingerprint, name, profileID, lastSeen, neverAsk), plus askOnNewNetwork (the master switch for automatic switching) and defaultAction (ask, keepCurrent, useProfile or noProfile). |
dnsEncryption |
Object | Choices only, never a system DNS configuration: provider (quad9, cloudflare, google, nextDNS with a profile id, or custom), transport (https or tls), exceptions. |
pauseNetwork |
String, optional | The network fingerprint a pause-until-network-change is tied to. |
onboarding |
Object | Setup choices: passedIntroduction, completed, the trust groups offered during setup (macOSServices, iCloudServices, appleApps, existingApps, localNetwork, existingAppNames). |
downloadedAllowGroups |
Array | Allow-list subscriptions staged for review: each group has id, name and entries (each with id, remote, state of pending or accepted). Pending entries are not rules. |
notifications |
Object | One switch per notification kind, all boolean: approvalNeeded, operationalProblems, securityEvents, ruleActivity, profileChanged, blocklistUpdateSucceeded, blocklistUpdateFailed, reviewWaiting, deniedDuringOSUpdate, heavyTelemetry. |
assistantKeepsHistory |
Boolean | Whether Assistant transcripts stay on this Mac. Turning it off preserves existing conversations. |
menuBar |
Object | warnWhenFilterOff (default true), flashOnBlock (default false), countLocalTraffic (default false). |
monitor |
Object | historyCapacity (default 5,000, range 1,000 to 50,000) and autoUpdateGeoDatabase (default true). |
telemetryBlock |
Object | The “Block known telemetry” settings: enabled (default off), categories, apps (signing ids of apps with telemetry turned off), mutedApps, appIdentities. |
telemetryDatabaseVersion |
String | The tracker database version the telemetry rules were last built from; empty until the first reconcile. |
isLocked |
Boolean | Whether the settings lock is on. Default false. |
lockRuleEditing |
Boolean | Default true. See Locking settings. |
lockProfileSwitching |
Boolean | Default true. |
lockSettingsEditing |
Boolean | Default true. |
allowOutboundctlChanges |
Boolean | Default true. |
alertDetailLevel |
String | compact or full. Default compact. |
confirmWithReturnEscape |
Boolean | Whether Return and Escape answer an alert. Default true. |
alertsTakeKeyboardFocus |
Boolean | Off by default: a new alert becomes the key window once its answers arm. |
denyWarningSuppressions |
Array of strings | Deny warnings you chose not to show again. |
markNewRulesUnapproved |
Boolean | Default false. See Review. |
approveRulesOnSelect |
Boolean | Default true. |
dataRateUnit |
String | bytes or bits. Default bytes. |
extraLocalRanges |
Array of strings | Extra CIDR ranges counted as local network, on top of the standard private ranges. |
The policy object
Section titled “The policy object”| Key | Type | Meaning |
|---|---|---|
mode |
String | alert, silentAllow or silentDeny. |
managedRulesOverride |
Boolean | Managed rules beat everything except priority rules. |
usersMayEditRules |
Boolean | Whether the user may edit rules. |
disableAlerts |
Boolean | ask becomes deny in silentDeny mode, allow otherwise, without an alert. |
approvedBlocklists |
Array | Subscriptions accepted with the enforced configuration (id, url, action, pinnedSHA256). |
extraLocalRanges |
Array of strings | Mirrored here so the filter sees them. |
ruleFeedURL |
URL, optional | The feed address. Nothing in the app sets it in the stored policy, so it stays absent here: the feed address comes from the managed preference and is laid over the policy at run time. |
ruleFeedPinnedSHA256 |
String, optional | Likewise for the feed pin. |
tcpAlertTimeout |
Number | Seconds a TCP connection waits for an answer before the timeout action applies: 15 to 120, default 60. |
activeProfileID |
UUID, optional | The active profile; nil = no profile. |
timeoutAction |
String | What an unanswered flow gets: allow or deny (ask reads as allow). Default allow. |
requireApprovalToUnblock |
Boolean | Default true. Changes that unblock something need approval with Touch ID or the login password. |
pausedUntil |
Date, optional | A timed pause deadline. |
pauseBootSessionUUID |
String, optional | Ties a pause to the current boot, so the next restart ends it. |
appAbsentAction |
String, optional | What a connection gets when the app that would answer is absent. nil = same as the timeout action. |