Add macOS sandbox support for notifications#3
Conversation
1062cab to
cdbc445
Compare
cdd1675 to
f32e532
Compare
Add allowNotifications parameter to enable notification access for tools like terminal-notifier and osascript display notification. When enabled, grants access to: - Window server (com.apple.windowserver.active) - TCC system (com.apple.tccd.system) - Dock server (com.apple.dock.server) - Core services (coreservicesd, appleevents) - HID control and file issue extensions This allows sandboxed processes to display desktop notifications. Generated with AI Co-Authored-By: Claude Code
f32e532 to
984cb5b
Compare
|
will this allow sounds also? |
…ntifier Instead of broadly allowing mach-lookup for notification services, use with-filter (signing-identifier "terminal-notifier") to restrict access to only the terminal-notifier binary. Also refines the set of allowed services to the minimal core notification pipeline. Generated with AI Co-Authored-By: Claude Code
Systematically tested removing each mach-lookup permission one at a time. Only usernoted.client and windowserver.active are required for terminal-notifier to work. Removed tccd.system, CARenderServer, and dock.server which are not needed. Generated with AI Co-Authored-By: Claude Code
📝 WalkthroughWalkthroughAdds an optional Changes
sequenceDiagram
participant Config as Config (SandboxRuntimeConfig)
participant Manager as SandboxManager
participant Wrapper as wrapCommandWithSandboxMacOS
participant Profile as generateSandboxProfile
participant Command as Sandboxed Command
Config->>Manager: provide runtime config (allowNotifications?)
Manager->>Wrapper: wrap command, pass allowNotifications
Wrapper->>Profile: request sandbox profile (allowNotifications)
Profile-->>Wrapper: sandbox profile (includes notification rules if true)
Wrapper->>Command: execute command under generated sandbox profile
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/sandbox/macos-sandbox-utils.ts (1)
23-39: 🧹 Nitpick | 🔵 TrivialUse a type alias instead of interface for
MacOSSandboxParams.Per TS-001,
typeis preferred overinterfacefor flexibility.♻️ Suggested refactor
-export interface MacOSSandboxParams { +export type MacOSSandboxParams = { command: string needsNetworkRestriction: boolean httpProxyPort?: number socksProxyPort?: number allowUnixSockets?: string[] allowAllUnixSockets?: boolean allowLocalBinding?: boolean readConfig: FsReadRestrictionConfig | undefined writeConfig: FsWriteRestrictionConfig | undefined ignoreViolations?: IgnoreViolationsConfig | undefined allowPty?: boolean allowGitConfig?: boolean enableWeakerNetworkIsolation?: boolean allowClipboard?: boolean allowNotifications?: boolean binShell?: string -} +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/sandbox/macos-sandbox-utils.ts` around lines 23 - 39, Replace the exported interface MacOSSandboxParams with an exported type alias named MacOSSandboxParams that uses an object type with the same properties (command, needsNetworkRestriction, httpProxyPort, socksProxyPort, allowUnixSockets, allowAllUnixSockets, allowLocalBinding, readConfig, writeConfig, ignoreViolations, allowPty, allowGitConfig, enableWeakerNetworkIsolation, allowClipboard, allowNotifications, binShell); keep all optional markers (?) and the same referenced types (FsReadRestrictionConfig, FsWriteRestrictionConfig, IgnoreViolationsConfig) and export modifier so all existing usages of MacOSSandboxParams continue to work.
📜 Review details
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
src/sandbox/macos-sandbox-utils.tssrc/sandbox/sandbox-config.tssrc/sandbox/sandbox-manager.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts
⚙️ CodeRabbit configuration file
**/*.ts: ## TypeScript StandardsTypes and Interfaces (TS-001)
- Prefer
typeoverinterfacefor flexibility.- Use unions (
|) for "one of many", intersections (&) for combining types.- Use
Merge<T, U>when overriding conflicting properties.- Use
neverfor mutually exclusive (XOR) property patterns.- Don't declare interfaces multiple times (they merge implicitly).
Type Safety (TS-014)
- Infer types using
ReturnType,Parameters,typeofrather than redeclaring.- Use runtime narrowing with type guards instead of casting with
as.- Use
unknowninstead ofanyto force proper type handling.- Constrain generics with
extendsto enforce requirements.TypeScript Patterns (TS-015)
- Use
as constto preserve literal types in constants.- Use
as const satisfiesfor type-checked constants with literal preservation.- Avoid enums; use union types or const objects instead.
- Use explicit return types at module boundaries (exported functions); let TS infer for internal functions.
- Declare modules in
index.d.tsfor untyped packages.OpenAPI Codegen (TS-009)
- Generate TypeScript types from Swagger/OpenAPI specs using
@zocdoc/client-types.- Run Swagger Validation before merging.
- Don't define enums as numbers in Swagger; use strings.
Import Patterns (TS-020)
- Import specific functions rather than entire modules to reduce bundle size
(e.g.,import debounce from 'lodash/debounce'notimport { debounce } from 'lodash').- Use ES6 imports over CommonJS
require().GraphQL (TS-006)
- Write GQL operations in separate
.gql.tsfiles with unique descriptive names.- Use naming convention
phi-patient-<name>.gql.tsorphi-user-<name>.gql.tsfor PHI.- Run
codegen:gqlafter creating operations.- Use custom hooks wrapping
useQuery/useMutationwithonErrorandonComplete.- Don't use Apollo hooks directly in components.
Files:
src/sandbox/macos-sandbox-utils.tssrc/sandbox/sandbox-config.tssrc/sandbox/sandbox-manager.ts
🔇 Additional comments (4)
src/sandbox/sandbox-manager.ts (1)
580-599: allowNotifications is correctly passed into the macOS wrapper.src/sandbox/macos-sandbox-utils.ts (3)
323-353: DefaultingallowNotificationsto false preserves opt‑in behavior.
505-518: Scoped notification rule is appropriately minimal.
662-711: allowNotifications is correctly threaded into profile generation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/sandbox/sandbox-config.ts`:
- Around line 224-229: The current description for allowNotifications overstates
permissions; update the allowNotifications z.boolean().optional().describe(...)
text to accurately state that it only grants mach-lookup access scoped to
terminal-notifier for com.apple.usernoted.client and
com.apple.windowserver.active (i.e., enables notification-related mach-lookup,
not full TCC/Dock or broad system access). Locate the allowNotifications schema
entry in sandbox-config.ts and replace the description string to reflect the
limited mach-lookup scope and macOS-only applicability.
In `@src/sandbox/sandbox-manager.ts`:
- Around line 474-476: The helper function getAllowNotifications explicitly
annotates its return type as boolean; since it's internal and not exported,
remove the explicit ": boolean" annotation from the getAllowNotifications
declaration so TypeScript will infer the return type from the implementation
(return config?.allowNotifications ?? false); update the function signature in
sandbox-manager.ts for the function named getAllowNotifications accordingly.
---
Outside diff comments:
In `@src/sandbox/macos-sandbox-utils.ts`:
- Around line 23-39: Replace the exported interface MacOSSandboxParams with an
exported type alias named MacOSSandboxParams that uses an object type with the
same properties (command, needsNetworkRestriction, httpProxyPort,
socksProxyPort, allowUnixSockets, allowAllUnixSockets, allowLocalBinding,
readConfig, writeConfig, ignoreViolations, allowPty, allowGitConfig,
enableWeakerNetworkIsolation, allowClipboard, allowNotifications, binShell);
keep all optional markers (?) and the same referenced types
(FsReadRestrictionConfig, FsWriteRestrictionConfig, IgnoreViolationsConfig) and
export modifier so all existing usages of MacOSSandboxParams continue to work.
AudioComponentRegistrar and audiohald are needed for terminal-notifier to play notification sounds through the macOS audio subsystem. Generated with AI Co-Authored-By: Claude Code
yes |
| profile.push( | ||
| '; Notification access - scoped to terminal-notifier via signing-identifier', | ||
| '; Minimal set: only core notification pipeline services', | ||
| '(with-filter (signing-identifier "terminal-notifier")', |
There was a problem hiding this comment.
This is a way we can add SOME, BUT NOT COMPLETE safety around limiting restrictions to certain processes. With processes that are better signed than terminal-notifier, we can check the signing against a team id. As I mention in the pr description, there is a hole in this security posture, but I deem it acceptable.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/sandbox/macos-sandbox-utils.ts (1)
23-39: 🧹 Nitpick | 🔵 TrivialPrefer a
typealias forMacOSSandboxParams.This interface update is a good moment to align with the TS-001 guideline.
♻️ Suggested refactor
-export interface MacOSSandboxParams { - command: string - needsNetworkRestriction: boolean - httpProxyPort?: number - socksProxyPort?: number - allowUnixSockets?: string[] - allowAllUnixSockets?: boolean - allowLocalBinding?: boolean - readConfig: FsReadRestrictionConfig | undefined - writeConfig: FsWriteRestrictionConfig | undefined - ignoreViolations?: IgnoreViolationsConfig | undefined - allowPty?: boolean - allowGitConfig?: boolean - enableWeakerNetworkIsolation?: boolean - allowClipboard?: boolean - allowNotifications?: boolean - binShell?: string -} +export type MacOSSandboxParams = { + command: string + needsNetworkRestriction: boolean + httpProxyPort?: number + socksProxyPort?: number + allowUnixSockets?: string[] + allowAllUnixSockets?: boolean + allowLocalBinding?: boolean + readConfig: FsReadRestrictionConfig | undefined + writeConfig: FsWriteRestrictionConfig | undefined + ignoreViolations?: IgnoreViolationsConfig | undefined + allowPty?: boolean + allowGitConfig?: boolean + enableWeakerNetworkIsolation?: boolean + allowClipboard?: boolean + allowNotifications?: boolean + binShell?: string +}As per coding guidelines, Prefer
typeoverinterfacefor flexibility.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/sandbox/macos-sandbox-utils.ts` around lines 23 - 39, Replace the exported interface MacOSSandboxParams with an exported type alias named MacOSSandboxParams that has the same object shape (all the same property names and optional modifiers) so references to MacOSSandboxParams, including uses of fields like command, needsNetworkRestriction, httpProxyPort, socksProxyPort, allowUnixSockets, allowAllUnixSockets, allowLocalBinding, readConfig (FsReadRestrictionConfig), writeConfig (FsWriteRestrictionConfig), ignoreViolations, allowPty, allowGitConfig, enableWeakerNetworkIsolation, allowClipboard, allowNotifications, and binShell continue to work; update any places importing or referencing MacOSSandboxParams only if they rely on interface-specific behavior (none expected) and ensure the export remains `export` so the type alias replaces the previous interface without changing its shape or optionality.
📜 Review details
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
src/sandbox/macos-sandbox-utils.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts
⚙️ CodeRabbit configuration file
**/*.ts: ## TypeScript StandardsTypes and Interfaces (TS-001)
- Prefer
typeoverinterfacefor flexibility.- Use unions (
|) for "one of many", intersections (&) for combining types.- Use
Merge<T, U>when overriding conflicting properties.- Use
neverfor mutually exclusive (XOR) property patterns.- Don't declare interfaces multiple times (they merge implicitly).
Type Safety (TS-014)
- Infer types using
ReturnType,Parameters,typeofrather than redeclaring.- Use runtime narrowing with type guards instead of casting with
as.- Use
unknowninstead ofanyto force proper type handling.- Constrain generics with
extendsto enforce requirements.TypeScript Patterns (TS-015)
- Use
as constto preserve literal types in constants.- Use
as const satisfiesfor type-checked constants with literal preservation.- Avoid enums; use union types or const objects instead.
- Use explicit return types at module boundaries (exported functions); let TS infer for internal functions.
- Declare modules in
index.d.tsfor untyped packages.OpenAPI Codegen (TS-009)
- Generate TypeScript types from Swagger/OpenAPI specs using
@zocdoc/client-types.- Run Swagger Validation before merging.
- Don't define enums as numbers in Swagger; use strings.
Import Patterns (TS-020)
- Import specific functions rather than entire modules to reduce bundle size
(e.g.,import debounce from 'lodash/debounce'notimport { debounce } from 'lodash').- Use ES6 imports over CommonJS
require().GraphQL (TS-006)
- Write GQL operations in separate
.gql.tsfiles with unique descriptive names.- Use naming convention
phi-patient-<name>.gql.tsorphi-user-<name>.gql.tsfor PHI.- Run
codegen:gqlafter creating operations.- Use custom hooks wrapping
useQuery/useMutationwithonErrorandonComplete.- Don't use Apollo hooks directly in components.
Files:
src/sandbox/macos-sandbox-utils.ts
🔇 Additional comments (3)
src/sandbox/macos-sandbox-utils.ts (3)
323-353: DefaultingallowNotificationsto false keeps compatibility.The parameter plumbing and default preserve existing behavior while enabling the new feature.
371-389: Mach IPC audio service additions look correct.The added AudioComponentRegistrar/audiohald entries are consistent with allowing audio playback.
664-713: Propagation ofallowNotificationsinto profile generation is clean.The flag is threaded consistently from params into the sandbox profile.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/sandbox/macos-sandbox-utils.ts`:
- Around line 507-519: The sandbox profile currently scopes notification access
using (signing-identifier "terminal-notifier") which is spoofable; update the
rule built under allowNotifications/profile to use a certificate-based
requirement that ties identifier + Team ID (e.g., an anchor + identifier
"terminal-notifier" and certificate leaf[subject.OU] = "<TEAM_ID>") instead of
plain signing-identifier, or alternatively run codesign -dv to extract the Team
ID at runtime and interpolate it into the requirement; modify the string(s)
pushed into profile where the lines contain '(with-filter (signing-identifier
"terminal-notifier")' so they emit the certificate-based requirement and keep
the rest of the mach-lookup entries intact.
---
Outside diff comments:
In `@src/sandbox/macos-sandbox-utils.ts`:
- Around line 23-39: Replace the exported interface MacOSSandboxParams with an
exported type alias named MacOSSandboxParams that has the same object shape (all
the same property names and optional modifiers) so references to
MacOSSandboxParams, including uses of fields like command,
needsNetworkRestriction, httpProxyPort, socksProxyPort, allowUnixSockets,
allowAllUnixSockets, allowLocalBinding, readConfig (FsReadRestrictionConfig),
writeConfig (FsWriteRestrictionConfig), ignoreViolations, allowPty,
allowGitConfig, enableWeakerNetworkIsolation, allowClipboard,
allowNotifications, and binShell continue to work; update any places importing
or referencing MacOSSandboxParams only if they rely on interface-specific
behavior (none expected) and ensure the export remains `export` so the type
alias replaces the previous interface without changing its shape or optionality.
| // Notification access - SCOPED TO TERMINAL-NOTIFIER ONLY using signing-identifier | ||
| if (allowNotifications) { | ||
| profile.push( | ||
| '; Notification access - scoped to terminal-notifier via signing-identifier', | ||
| '; Minimal set: only core notification pipeline services', | ||
| '(with-filter (signing-identifier "terminal-notifier")', | ||
| ' (allow mach-lookup', | ||
| ' (global-name "com.apple.usernoted.client")', | ||
| ' (global-name "com.apple.windowserver.active")', | ||
| ' )', | ||
| ')', | ||
| '', | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
macOS sandbox seatbelt signing-identifier security spoofing ad-hoc signing
💡 Result:
On macOS, Seatbelt/App Sandbox policies can match a process by its code signing identifier via the SBPL predicate (signing-identifier "...") (Apple uses this in platform sandbox policy rules/exceptions). [1]
Why this is spoofable (especially with ad-hoc signing)
- The code signing identifier is just a string embedded in the code signature; requirement language can test
identifier "com.example.app"for exact equality. [3] - When signing, you can explicitly set that identifier with
codesign -i <identifier>. If you don’t, it’s derived from the bundle’sInfo.plist(typicallyCFBundleIdentifier) or the filename. [2] - Ad-hoc signing (
codesign -s - ...) does not bind code to a real developer identity/certificate chain; many security tools treat ad-hoc as effectively “unsigned” because it provides no publisher authenticity. [5] (In Apple’s requirement language terms, ad-hoc signatures have no certificates, so certificate-based constraints can’t validate an identity.) [3]
Consequence: If a sandbox/Seatbelt rule gates privilege using only signing-identifier, an attacker can often re-sign their own binary (including ad-hoc) with the same identifier string and satisfy that check. (This is the core “signing-identifier spoofing” issue.)
What does provide a stronger boundary
To avoid spoofing, policies/authorization should not rely on identifier alone; they should also bind to something that can’t be forged by ad-hoc signing, such as:
- a certificate/anchor-based requirement (Apple-issued chain / Team identity) rather than just identifier, leveraging requirement language’s certificate constraints (which fail for ad-hoc). [3]
- restricted entitlements that must be authorized (via provisioning) specifically to prevent other developers from impersonating an app for sensitive resources (Apple explicitly calls out keychain groups as an example). [6]
- other non-forgeable attributes sometimes used in sandbox policy, like platform-binary status, etc. (visible in Apple’s own policy patterns). [1]
Related: “same identifier” makes components indistinguishable
Apple documentation notes that giving multiple components the same code signing identifier (via matching CFBundleIdentifier or codesign -i) can make them validate “as the same program” for purposes like keychain access—useful when intentional, dangerous if assumed to imply a verified developer identity. [4]
🌐 Web query:
macOS seatbelt team-identifier predicate documentation
💡 Result:
Apple doesn’t publish official/public documentation for SBPL (Seatbelt) filter predicates like team-identifier. The public docs (sandbox(7), sandbox-exec(1)) describe the existence of the sandbox and how profiles are applied, but not the full predicate vocabulary (Apple treats most of the low-level sandbox(7) API surface as private/undocumented). [1], [2]
What’s available instead:
- Reverse-engineered predicate lists / references: Mark Rowe’s deep-dive explains SBPL evaluation and links to extracted lists of supported filter predicates (for macOS 15.x), including code-identity related filters like
signing-identifier(but Apple still doesn’t document these). [2] - System-shipped profiles to grep for real usage: macOS ships many
.sbprofiles in/usr/share/sandbox, which is often the most practical “documentation” source. [3] - How to get the Team ID for any binary (for use in identity matching approaches):
codesign -dv <path>showsTeamIdentifier=…. [4], [5]
If you share what exact SBPL snippet you’re looking at (e.g., where you saw team-identifier used), I can point to the closest documented equivalent and/or how that predicate is typically replaced (often via other identity predicates like signing-identifier). [2]
Sources
[1] Chromium: “The Mac Sandbox” (Seatbelt / sandbox(7) overview) (chromium.googlesource.com)
[2] Mark Rowe: “Sandboxing on macOS” (SBPL, private APIs, predicate discussion + links to extracted predicate lists) (bdash.net.nz)
[3] s7ephen repo notes using /usr/share/sandbox as reference profiles (github.com)
[4] Addigy: finding TeamIdentifier= via codesign -dv (support.addigy.com)
[5] Jamf: locating Identifier and TeamID via codesign -dv --verbose=4 (support.jamf.com)
Citations:
- 1: https://chromium.googlesource.com/chromium/src/%2B/HEAD/sandbox/mac/README.md?utm_source=openai
- 2: https://bdash.net.nz/posts/sandboxing-on-macos/
- 3: https://github.com/s7ephen/OSX-Sandbox--Seatbelt--Profiles?utm_source=openai
- 4: https://support.addigy.com/hc/en-us/articles/4403542583187-How-To-Get-The-Team-ID-Bundle-ID-and-Code-Requirement?utm_source=openai
- 5: https://support.jamf.com/en/articles/11021267-locate-identifier-and-team-identifier?utm_source=openai
🌐 Web query:
macOS sandbox code signing security signing-identifier vs team-identifier strength
💡 Result:
-
“team-identifier” (aka Team ID, 10‑char) is an identity scope for the developer/team extracted from the Apple-issued signing certificate and also appears in entitlements as
com.apple.developer.team-identifier. Apple uses it as a coarse trust boundary for “same-vendor” checks (for example, which third‑party libraries an app can load, and which apps can participate in certain cross-app capabilities when their Team IDs match). [1][2] -
“signing-identifier” in this context maps to the code signature’s Identifier (typically your bundle identifier) that you can match in a code requirement via
identifier "com.example.app". This is an app-specific identity check (exact match only). [3]
Strength (security binding)
From strongest (most specific) to weakest (most permissive):
-
Team ID + Identifier (recommended when you need to trust one specific app)
- Typical pattern:
anchor apple generic and identifier "…" and certificate leaf[subject.OU] = "TEAMID"(OU is the Team ID). This pins both the vendor and the exact app identity. [4][3]
- Typical pattern:
-
Identifier only
- Pins a specific bundle ID, but (depending on how the requirement is constructed) may be less explicit about which Apple-issued developer identity must have signed it (you generally still want an Apple anchor / cert constraints). [3]
-
Team ID only
- Trusts any code signed by that Team ID (all apps/extensions from that developer/team). This is intentionally broader, and therefore weaker, but useful for admin allowlisting at “vendor” granularity. [5][2]
Practical guidance
- Use Team ID matching when the policy is “trust this developer/vendor broadly” (common in MDM allowlists). [5]
- Use Identifier + Team ID when the policy is “trust only this exact app (from this vendor)”—it minimizes the blast radius if the vendor ships additional tools you didn’t intend to trust. [4][3]
Sources: Apple Entitlements Troubleshooting (TN2415) [1], Apple Platform Security (team identifier for code validation) [2], Apple Code Signing Requirement Language (the identifier constraint) [3], example designated requirement including certificate leaf[subject.OU] = TEAMID [4], Microsoft Intune macOS extension allowlisting (Team ID vs specific extensions) [5].
Use certificate-based requirement (Team ID + code signing) instead of signing-identifier alone.
signing-identifier can be spoofed through ad-hoc code signing, which lacks certificate chain binding. Any sandboxed process could re-sign itself with the same identifier and gain these mach-lookup rights. Replace with certificate-based requirements (e.g., anchor apple generic and identifier "terminal-notifier" and certificate leaf[subject.OU] = "<TEAM_ID>") to bind the rule to both the specific app and its legitimate Apple-issued developer certificate. Alternatively, extract and validate the team identifier via codesign -dv before applying the profile.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/sandbox/macos-sandbox-utils.ts` around lines 507 - 519, The sandbox
profile currently scopes notification access using (signing-identifier
"terminal-notifier") which is spoofable; update the rule built under
allowNotifications/profile to use a certificate-based requirement that ties
identifier + Team ID (e.g., an anchor + identifier "terminal-notifier" and
certificate leaf[subject.OU] = "<TEAM_ID>") instead of plain signing-identifier,
or alternatively run codesign -dv to extract the Team ID at runtime and
interpolate it into the requirement; modify the string(s) pushed into profile
where the lines contain '(with-filter (signing-identifier "terminal-notifier")'
so they emit the certificate-based requirement and keep the rest of the
mach-lookup entries intact.
Problem
Tools that display desktop notifications (
terminal-notifier) and play sounds (afplay) fail in the macOS sandbox due to missing permissions for window server, notification, and audio system services.Solution
Notification permissions (scoped to terminal-notifier)
Added
allowNotificationsconfiguration parameter that grants scoped access to notification services, restricted to theterminal-notifierbinary via code signing identity:Uses
with-filter (signing-identifier "terminal-notifier")so only the terminal-notifier process receives these permissions. Other sandboxed processes (including the AI agent itself) are denied access. Verified by confirmingosascript -e 'display notification "test"'is blocked under the same profile.Audio permissions (global)
Added three audio services to the global mach-lookup allow list, enabling
afplay(and any sandboxed process) to play sounds:com.apple.audio.AudioComponentRegistrarcom.apple.audio.audiohaldcom.apple.tccdis denied)com.apple.audio.systemsoundserverThese are not gated behind
allowNotifications— audio playback is considered safe for all sandboxed processes. The theoretical risk is mic recording viaaudiohald, but macOS TCC enforcement (which the sandbox denies access to) prevents mic access without an OS-level permission prompt that the agent cannot grant itself.Permission minimization
Only 2 scoped mach services are required for notifications (out of 14 that generate sandbox violations):
com.apple.usernoted.clientcom.apple.windowserver.activecom.apple.tccd.systemcom.apple.CARenderServercom.apple.dock.serverWhy
signing-identifierand not path-based filteringPath-based process predicates (
process-path,process-path-regex) were tested exhaustively (7 different approaches) and are non-functional insandbox-execuser profiles. They parse without error but have zero filtering effect.signing-identifieris the only process-scoping predicate that works, verified by the kernel's code signing subsystem rather than sandbox path matching.Changes
3 files changed, 33 insertions:
sandbox-config.ts— AddedallowNotificationsboolean toSandboxRuntimeConfigSchemasandbox-manager.ts— AddedgetAllowNotifications()helper and wired it intowrapWithSandbox()macos-sandbox-utils.ts— AddedallowNotificationsparameter threading, audio mach services (AudioComponentRegistrar,audiohald) to global allow list, and scopedterminal-notifiernotification rules viasigning-identifierKnown limitation: signing-identifier spoofing
signing-identifierchecks the code signing identifier string but does not validate the signing authority (Team ID). This means a process could spoof the identity:codesign:codesign -s - -i "terminal-notifier" ./malicious_binaryproduces a binary matching the filtergcc -o terminal-notifier evil.cproduces a match without any explicit signingWhy we can't fully mitigate this
sandbox-execuser profiles (see above)codesignvia(deny process-exec (literal "/usr/bin/codesign"))raises the bar but doesn't prevent the compiler-based pathprocess-execfrom writable directories would break the sandbox's core purpose of running build commandsRisk assessment
Exploiting this requires the agent to: (a) have access to a compiler, (b) be allowed to execute binaries from writable directories, and (c) deliberately craft a binary to abuse
windowserver.active. The services exposed are:usernoted.client— low risk (can post notifications; no data exfiltration)windowserver.active— medium risk (could inspect windows or capture screen content, but requires active exploitation via compiled code)This is defense-in-depth, not a hard security boundary. The scoping prevents accidental or incidental use of these services by other sandboxed processes.
Usage
{ "allowNotifications": true }Testing
terminal-notifier -title 'Test' -message 'test'— displays notificationosascript -e 'display notification "test"'— denied (proves scoping works)allowNotifications: false(default) — terminal-notifier blockedafplay /System/Library/Sounds/Glass.aiff— plays soundTest plan
terminal-notifierworks withallowNotifications: trueallowNotifications: false(default)afplaycan play sounds in the sandbox