Skip to content

Add macOS sandbox support for notifications#3

Merged
eugene-yao-zocdoc merged 4 commits into
mainfrom
ey_add_notifications_support
Feb 18, 2026
Merged

Add macOS sandbox support for notifications#3
eugene-yao-zocdoc merged 4 commits into
mainfrom
ey_add_notifications_support

Conversation

@eugene-yao-zocdoc

@eugene-yao-zocdoc eugene-yao-zocdoc commented Feb 13, 2026

Copy link
Copy Markdown

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 allowNotifications configuration parameter that grants scoped access to notification services, restricted to the terminal-notifier binary via code signing identity:

(with-filter (signing-identifier "terminal-notifier")
  (allow mach-lookup
    (global-name "com.apple.usernoted.client")
    (global-name "com.apple.windowserver.active")))

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 confirming osascript -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:

Service Purpose Security risk
com.apple.audio.AudioComponentRegistrar Audio codec plugin loading None — codec registry, not a hardware interface
com.apple.audio.audiohald Audio Hardware Abstraction Layer daemon Low — mic recording is blocked by TCC (com.apple.tccd is denied)
com.apple.audio.systemsoundserver System alert sounds (pre-existing) None

These are not gated behind allowNotifications — audio playback is considered safe for all sandboxed processes. The theoretical risk is mic recording via audiohald, 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):

Service Purpose Required?
com.apple.usernoted.client Notification daemon YES — hangs without
com.apple.windowserver.active Window server for GUI display YES — hangs without
com.apple.tccd.system TCC privacy checks No — fails gracefully
com.apple.CARenderServer Core Animation rendering No — fails gracefully
com.apple.dock.server Dock integration No — fails gracefully
9 others (hid-control, mach-register, etc.) Various system services No — fails gracefully

Why signing-identifier and not path-based filtering

Path-based process predicates (process-path, process-path-regex) were tested exhaustively (7 different approaches) and are non-functional in sandbox-exec user profiles. They parse without error but have zero filtering effect. signing-identifier is 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 — Added allowNotifications boolean to SandboxRuntimeConfigSchema
  • sandbox-manager.ts — Added getAllowNotifications() helper and wired it into wrapWithSandbox()
  • macos-sandbox-utils.ts — Added allowNotifications parameter threading, audio mach services (AudioComponentRegistrar, audiohald) to global allow list, and scoped terminal-notifier notification rules via signing-identifier

Known limitation: signing-identifier spoofing

signing-identifier checks the code signing identifier string but does not validate the signing authority (Team ID). This means a process could spoof the identity:

  1. Via codesign: codesign -s - -i "terminal-notifier" ./malicious_binary produces a binary matching the filter
  2. Via compiler: On macOS 11+, the linker auto-signs output binaries with an identifier derived from the filename — gcc -o terminal-notifier evil.c produces a match without any explicit signing

Why we can't fully mitigate this

  • Team ID validation is not possible because Homebrew's terminal-notifier is adhoc-signed (no Team ID exists)
  • Path-based filtering doesn't work in sandbox-exec user profiles (see above)
  • Blocking codesign via (deny process-exec (literal "/usr/bin/codesign")) raises the bar but doesn't prevent the compiler-based path
  • Blocking process-exec from writable directories would break the sandbox's core purpose of running build commands

Risk 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.clientlow risk (can post notifications; no data exfiltration)
  • windowserver.activemedium 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 notification
  • osascript -e 'display notification "test"' — denied (proves scoping works)
  • allowNotifications: false (default) — terminal-notifier blocked
  • afplay /System/Library/Sounds/Glass.aiff — plays sound

Test plan

  • Verify terminal-notifier works with allowNotifications: true
  • Verify notifications are blocked with allowNotifications: false (default)
  • Verify other processes (osascript) cannot use the scoped notification permissions
  • Verify afplay can play sounds in the sandbox
  • Test on Intel Mac (current testing on Apple Silicon only)

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
@eugene-yao-zocdoc eugene-yao-zocdoc force-pushed the ey_add_notifications_support branch from f32e532 to 984cb5b Compare February 16, 2026 22:19
@trevdev-zocdoc

Copy link
Copy Markdown

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
@eugene-yao-zocdoc eugene-yao-zocdoc marked this pull request as ready for review February 18, 2026 00:42
@coderabbitai

coderabbitai Bot commented Feb 18, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds an optional allowNotifications flag to sandbox configuration and propagates it through the sandbox manager to macOS sandbox utilities, conditionally including notification-related rules in generated macOS sandbox profiles.

Changes

Cohort / File(s) Summary
Configuration Schema
src/sandbox/sandbox-config.ts
Added optional allowNotifications?: boolean to SandboxRuntimeConfigSchema with description.
macOS Sandbox Implementation
src/sandbox/macos-sandbox-utils.ts
Extended MacOSSandboxParams and related function signatures to accept allowNotifications; when true, append a scoped notification-access block (terminal-notifier via signing-identifier) and extra global-name Mach IPC entries to the generated sandbox profile; default is false.
Sandbox Manager
src/sandbox/sandbox-manager.ts
Added getAllowNotifications() helper returning config?.allowNotifications ?? false and passed the flag into sandbox wrapper invocations for macOS (and propagated to Linux wrapper calls where present).
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
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 I nibble configs, tiny and bright,
A flag for pings now joins the night,
From manager's hand to macOS shell,
Notifications find their place to dwell,
Hoppity-hop—sandbox dreams take flight 🌙✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add macOS sandbox support for notifications' accurately reflects the main change—enabling notification permissions for the macOS sandbox with the new allowNotifications flag and related mach-lookup configuration.
Description check ✅ Passed The description comprehensively explains the problem (missing notification/audio permissions), the solution (allowNotifications parameter with signing-identifier scoping and audio services), and includes detailed testing, security considerations, and known limitations directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ey_add_notifications_support

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔵 Trivial

Use a type alias instead of interface for MacOSSandboxParams.

Per TS-001, type is preferred over interface for 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

📥 Commits

Reviewing files that changed from the base of the PR and between b6ef6cf and 7cfce99.

📒 Files selected for processing (3)
  • src/sandbox/macos-sandbox-utils.ts
  • src/sandbox/sandbox-config.ts
  • src/sandbox/sandbox-manager.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: ## TypeScript Standards

Types and Interfaces (TS-001)

  • Prefer type over interface for flexibility.
  • Use unions (|) for "one of many", intersections (&) for combining types.
  • Use Merge<T, U> when overriding conflicting properties.
  • Use never for mutually exclusive (XOR) property patterns.
  • Don't declare interfaces multiple times (they merge implicitly).

Type Safety (TS-014)

  • Infer types using ReturnType, Parameters, typeof rather than redeclaring.
  • Use runtime narrowing with type guards instead of casting with as.
  • Use unknown instead of any to force proper type handling.
  • Constrain generics with extends to enforce requirements.

TypeScript Patterns (TS-015)

  • Use as const to preserve literal types in constants.
  • Use as const satisfies for 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.ts for 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' not import { debounce } from 'lodash').
  • Use ES6 imports over CommonJS require().

GraphQL (TS-006)

  • Write GQL operations in separate .gql.ts files with unique descriptive names.
  • Use naming convention phi-patient-<name>.gql.ts or phi-user-<name>.gql.ts for PHI.
  • Run codegen:gql after creating operations.
  • Use custom hooks wrapping useQuery/useMutation with onError and onComplete.
  • Don't use Apollo hooks directly in components.

Files:

  • src/sandbox/macos-sandbox-utils.ts
  • src/sandbox/sandbox-config.ts
  • src/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: Defaulting allowNotifications to 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.

Comment thread src/sandbox/sandbox-config.ts
Comment thread src/sandbox/sandbox-manager.ts
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
@eugene-yao-zocdoc

Copy link
Copy Markdown
Author

will this allow sounds also?

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")',

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@trevdev-zocdoc trevdev-zocdoc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🔵 Trivial

Prefer a type alias for MacOSSandboxParams.

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 type over interface for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7cfce99 and 4e9631e.

📒 Files selected for processing (1)
  • src/sandbox/macos-sandbox-utils.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.ts

⚙️ CodeRabbit configuration file

**/*.ts: ## TypeScript Standards

Types and Interfaces (TS-001)

  • Prefer type over interface for flexibility.
  • Use unions (|) for "one of many", intersections (&) for combining types.
  • Use Merge<T, U> when overriding conflicting properties.
  • Use never for mutually exclusive (XOR) property patterns.
  • Don't declare interfaces multiple times (they merge implicitly).

Type Safety (TS-014)

  • Infer types using ReturnType, Parameters, typeof rather than redeclaring.
  • Use runtime narrowing with type guards instead of casting with as.
  • Use unknown instead of any to force proper type handling.
  • Constrain generics with extends to enforce requirements.

TypeScript Patterns (TS-015)

  • Use as const to preserve literal types in constants.
  • Use as const satisfies for 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.ts for 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' not import { debounce } from 'lodash').
  • Use ES6 imports over CommonJS require().

GraphQL (TS-006)

  • Write GQL operations in separate .gql.ts files with unique descriptive names.
  • Use naming convention phi-patient-<name>.gql.ts or phi-user-<name>.gql.ts for PHI.
  • Run codegen:gql after creating operations.
  • Use custom hooks wrapping useQuery/useMutation with onError and onComplete.
  • 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: Defaulting allowNotifications to 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 of allowNotifications into 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.

Comment on lines +507 to +519
// 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")',
' )',
')',
'',
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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’s Info.plist (typically CFBundleIdentifier) 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 .sb profiles 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> shows TeamIdentifier=…. [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:


🌐 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):

  1. 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]
  2. 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]
  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.

@eugene-yao-zocdoc eugene-yao-zocdoc merged commit 6e6413a into main Feb 18, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants