by

MakePass

AI Wallet Editor macOS iOS iPadOS

MakePass is a powerful Apple Wallet pass creator and editor. Create a pass from scratch, import an existing one, or use AI to turn a photo, PDF, or description into an event ticket, boarding pass, membership card, coupon, and more.

HomeControl

Menu for HomeKit macOS

HomeControl is the ultimate HomeKit companion on macOS. Manage and automate your smart home directly from the menu bar: check your home status, trigger scenes, adjust devices, or switch between multiple homes with a single click.

OverPicture

for Safari macOS iOS iPadOS

OverPicture is a Safari Extension that lets you play any web video in Picture-in-Picture mode.

BrowserMask

for Safari macOS iOS iPadOS

BrowserMask is a Safari Extension that lets you browse websites as if you were using another browser by changing the User Agent Safari uses. This way, you can make any website believe it’s being shown in a browser like Google Chrome or Microsoft Edge.

ChatShare

for WhatsApp macOS

ChatShare is the missing Share Extension for WhatsApp. It allows you to share text, links, photos, and videos from apps with share menu support, such as Safari, Photos, or Chrome.

HomeBot

for HomeKit macOS iOS iPadOS

HomeBot for HomeKit lets you automate your home in powerful new ways.

BrowserSwitch

for Safari macOS

BrowserSwitch is a Safari Extension that can seamlessly open any Safari page in other web browsers like Google Chrome or Firefox. BrowserSwitch supports both opening a page or switching it: opening it on an external browser and closing it in Safari.

AirWeight

for Bluetooth Scales macOS iOS iPadOS visionOS watchOS tvOS

AirWeight connects to your Xiaomi Mi Scale or SANITAS SBF70 Bluetooth scale, reads your weight in real time, computes your body mass index and body fat percentage, and saves the measurements in the Health app.

MakePDF

Document Merger macOS iOS iPadOS

MakePDF allows you to merge multiple files into a single PDF really quickly by simply selecting or dropping documents and images.

VoiceExpress

Audio to Text macOS iOS iPadOS

VoiceExpress lets you transcribe any voice message or audio file. It can convert audio to text in all languages supported by the system dictation service.

InstaReload

for Safari macOS

InstaReload is a Safari Extension that allows you to auto-reload any Safari webpage with a configurable reload interval.

MenuBot

Custom Menu Bar macOS

MenuBot allows you to customize your Mac menu bar using shortcuts. Each menu added with MenuBot is powered by a shortcut that runs at a specified interval. The shortcut output populates the menu: the first line defines the menu bar icon, while the remaining lines can include symbols, text, action links, and submenus.

Blog

MakePass CLI: Create Apple Wallet Passes from Terminal, Scripts and AI Agents

EN

MakePass – Terminal and AI Agents

MakePass includes a command-line interface (CLI) on Mac to create and customize Apple Wallet passes, find passes in Wallet, and export them from Terminal, scripts, or AI agents. Command-line automation requires MakePass Ultra.

Get Started

Open “Automate with Command Line…” in the MakePass app. This section includes the tool’s path, your Automation Token, and commands ready to copy into Terminal.

Set your Automation Token and open the command help (these paths assume MakePass is installed in Applications):

export MAKEPASS_AUTOMATION_TOKEN='YOUR_TOKEN'
/Applications/MakePass.app/Contents/MacOS/makepass-cli --help

Replace YOUR_TOKEN with the token shown in the app. Commands require this token to ensure they come from a trusted source; the environment variable supplies it for this Terminal session.

Tip: To run makepass-cli by name, create a symbolic link:

sudo mkdir -p /usr/local/bin
sudo ln -s /Applications/MakePass.app/Contents/MacOS/makepass-cli /usr/local/bin/makepass-cli

If /usr/local/bin is not in your PATH, add export PATH="/usr/local/bin:$PATH" to your shell profile (~/.zprofile for zsh), then open a new Terminal window and set the token again.

The examples below use makepass-cli. You can always substitute the full path instead of creating the link.

Create a Pass

Create a pass with a QR code and open it in your default pass app:

makepass-cli create-pass --barcode-format qr --barcode-payload 123456 --header-title Example --open

Create a boarding pass with custom fields:

makepass-cli create-pass --pass-style boarding-pass --transit-type air --barcode-payload 123456 --front-fields From:BCN To:VGO --open

Use makepass-cli create-pass --help for all the options, including images, colors, dates, and importing an existing pass as a template.

File access: the tool reads and writes files inside its sandbox. For automatic transfers to and from other folders, install the optional helper script using the installation command shown by makepass-cli --help.

To get the generated file’s path as JSON, add --json:

makepass-cli create-pass --barcode-format qr --barcode-payload 123456 --header-title Example --json

The result contains the generated file’s path. For example:

{"path":"/path/to/generated-pass.pkpass"}

--silence returns just that path. Creation and export use this same output format. The default file is temporary; use --output to choose a destination for files you want to keep. Opening a pass does not add it to Wallet automatically.

Find and Export Passes

Find passes containing “Boarding” in their names, descriptions, identifiers, or fields, ignoring capitalization:

makepass-cli list-passes-in-wallet --filter-query Boarding --json

The output is an array of passes exposed by Wallet to MakePass, or [] if nothing matches; it is not a complete inventory of Wallet on another device. Records include id, name, organization, description, pass_type_identifier, and serial_number; relevant_date is included when available.

You can use jq to parse, filter, and transform the JSON output (install jq). For example, keep only the pass identifier, name, and organization:

makepass-cli list-passes-in-wallet --filter-query Boarding --json |
  jq '[.[] | {id, name, organization}]'

Illustrative output (the identifier is a placeholder):

[
  {"id":"PASS_ID", "name":"Boarding Pass", "organization":"Example Airline"}
]

Export a pass using an identifier returned by that command:

makepass-cli export-pass-from-wallet PASS_ID

Export Matching Passes in Batch

This Bash script finds matching passes, exports each one, and prints the resulting file paths. Set the token first; the script requires jq and the file-access helper described above so the tool can write to the selected folder:

#!/bin/bash
set -euo pipefail

output_directory="$HOME/Downloads/Wallet Exports"
mkdir -p "$output_directory"
passes=$(makepass-cli list-passes-in-wallet --filter-query Boarding --json)

jq -r '.[].id' <<< "$passes" | while IFS= read -r pass_id; do
  makepass-cli export-pass-from-wallet "$pass_id" \
    --output "$output_directory" --json | jq -r '.path'
done

The script prints one exported file path per line. If no passes match, nothing is exported. Existing destination files are replaced. A failed command returns a nonzero exit code and stops the script; completed exports remain. In JSON mode, execution errors go to standard error as {"error":{"message":"…"}}, keeping standard output available for results. Startup and argument errors may use plain text.

Use with AI Agents

An agent with terminal access to your Mac can build a workflow from the same commands. Give it the tool’s path and configure the token in its environment, then ask:

Create one membership pass for each row in this CSV. Use the member number as a QR code, put the name in a front field, and save the passes in a folder on my Desktop.

The agent reads create-pass --help, maps the CSV columns to options such as --barcode-payload and --front-fields, and runs create-pass --json for each row. It collects each returned path and reports any failures. Writing to the Desktop requires the file-access helper above.

HomeBot CLI: Control Your Home from Terminal, Scripts and AI Agents

EN

HomeBot – Terminal and AI Agents

HomeBot includes a command-line interface (CLI) on Mac to read HomeKit devices and sensors, trigger scenes, and change device settings from Terminal, scripts, or AI agents.

Get Started

Open “Automate with Command Line…” in the HomeBot app. This section includes the tool’s path, your Automation Token, and examples using your HomeKit items. Allow HomeKit access when prompted.

Set your Automation Token and open the command help (these paths assume HomeBot is installed in Applications):

export HOMEBOT_AUTOMATION_TOKEN='YOUR_TOKEN'
/Applications/HomeBot.app/Contents/MacOS/homebot-cli --help

Replace YOUR_TOKEN with the token shown in the app. Commands require this token to ensure they come from a trusted source; the environment variable supplies it for this Terminal session.

Tip: To run homebot-cli by name, create a symbolic link:

sudo mkdir -p /usr/local/bin
sudo ln -s /Applications/HomeBot.app/Contents/MacOS/homebot-cli /usr/local/bin/homebot-cli

If /usr/local/bin is not in your PATH, add export PATH="/usr/local/bin:$PATH" to your shell profile (~/.zprofile for zsh), then open a new Terminal window and set the token again.

The examples below use homebot-cli. You can always substitute the full path instead of creating the link.

Find Devices and Sensors

List your HomeKit items and their status. Add --verbose to include identifiers and supported limits and modes:

homebot-cli get-home-items

Read sensor measurements as structured JSON:

homebot-cli get-home-items --item-is-sensor true --json

--json returns an array of items with snake_case keys. Each record includes its name, identifier, home, status, and available actions or properties. Sensor values use the units listed in --help; temperatures are in degrees Celsius. Unavailable measurements and empty capability lists are omitted.

You can use jq to parse, filter, and transform the JSON output (install jq). For example, keep only temperature readings and their item and home names:

homebot-cli get-home-items --item-is-sensor true --json |
  jq '[.[] | select(.temperature_sensor_measurement != null) |
    {item_name, home_name, temperature_sensor_measurement}]'

Illustrative output:

[
  {
    "item_name": "Room Sensor",
    "home_name": "Main Home",
    "temperature_sensor_measurement": 26.5
  }
]

Run Actions

Replace the example home, device, and scene names with yours; names must match exactly, including capitalization. Turn on a lamp:

homebot-cli run-home-action --action-type switch-device-status --home-name 'Main Home' --item-name 'Desk Lamp' --activation-mode activate

Trigger a scene:

homebot-cli run-home-action --action-type trigger-scene --home-name 'Main Home' --item-name 'Good Night'

Check a brightness change without applying it:

homebot-cli run-home-action --action-type change-device-property --home-name 'Main Home' --item-name 'Desk Lamp' --property-type light-brightness --property-value 50 --dry-run

Remove --dry-run to apply the change. A successful dry run does not guarantee that the device will accept the write; check its supported properties and limits. Actions run on every matching item; use get-home-items with the same selection options to inspect the targets first. Names can match several items; use returned item_identifier values with --item-identifier to target specific ones. Each command’s --help lists its options and measurement units.

Add --json to an action to receive an array of the resulting items, with the same structure as get-home-items. With --dry-run, those records describe the existing state, not a predicted result.

All selected items are attempted. If any action fails, the command returns a nonzero exit code after finishing; successful changes remain applied. In JSON mode, no result array is returned on failure; the execution error is written to standard error as {"error":{"message":"…"}}. Startup and argument errors may use plain text. Re-read the affected items before retrying a failed batch.

For example, turn on a lamp and extract its resulting status:

homebot-cli run-home-action --action-type switch-device-status \
  --home-name 'Main Home' --item-name 'Desk Lamp' \
  --activation-mode activate --json |
  jq '[.[] | {item_name, item_is_active}]'

Illustrative output:

[
  {"item_name": "Desk Lamp", "item_is_active": true}
]

Combine Readings and Actions

This Bash script uses jq to turn on a fan when any temperature sensor in the selected room reports more than 25 °C. Set the token first and replace the example names with yours:

#!/bin/bash
set -euo pipefail

home_name='Main Home'
room_name='Living Room'
readings=$(homebot-cli get-home-items --home-name "$home_name" \
  --room-name "$room_name" --item-is-sensor true --json)

should_activate=$(jq 'any(.[]; .temperature_sensor_measurement != null and
  .temperature_sensor_measurement > 25)' <<< "$readings")

if [[ "$should_activate" == true ]]; then
  homebot-cli run-home-action --home-name "$home_name" \
    --room-name "$room_name" --item-type device --item-name Fan \
    --action-type switch-device-status --activation-mode activate --json
fi

It leaves the fan unchanged when there is no matching reading above the threshold. Readings use the available cached HomeKit status. The action returns its results as JSON. To schedule the script, configure the executable path and token in the scheduler’s environment too.

Use with AI Agents

An agent with terminal access to your Mac can use the same workflow. Give it the tool’s path and configure the token in its environment, then ask:

Find the lights in my living room, show me which are on, and set those lights to 30% brightness. Check the selection with a dry run before making changes.

The agent reads --help, queries get-home-items --json, and selects items supporting light-brightness. It uses their identifiers for run-home-action --dry-run, then applies the requested change and inspects the JSON results. No manual transcription of device names or status is needed.

Inside Claude Cowork: How Anthropic Runs Claude Code in a Local VM on Your Mac

EN

Claude Cowork

Claude Cowork is a feature of the Claude Desktop app that allows Claude to execute code, manipulate files, and perform complex tasks autonomously. This post documents a deep investigation into how it works under the hood, covering the architecture, security layers, and interesting implementation details.

TL;DR

Claude Cowork runs a full Linux virtual machine locally using native virtualization. Inside this VM, it executes Claude Code CLI within a multi-layered sandbox. Network access is restricted to a strict allowlist, and MCP servers from Claude Desktop are dynamically passed through to the VM. Multiple conversations share a single VM instance, but each gets its own isolated session.

Contents

Introduction

When you start a Cowork session in Claude Desktop, Claude can suddenly run Python scripts, process videos with ffmpeg, create PowerPoint presentations, and more. But how does it actually work? Is it a Docker container? A remote server?

Cowork is currently only available on macOS, so this investigation was conducted on that platform from both sides: from within the execution environment (Claude’s perspective) and from the host system (the user’s perspective), using Claude Code 1.1.799 (2e02b6). The findings reveal a thoughtful and robust architecture.

Architecture

The core architecture consists of three main layers:

┌────────────────────────────────────────────────────────────────────────┐
│                              macOS Host                                │
│                                                                        │
│   ┌────────────────────────────────────────────────────────────────┐   │
│   │  Claude.app (Electron)                                         │   │
│   │    ├── UI Renderer                                             │   │
│   │    ├── MCP Servers (Slack, Atlassian, etc.)                    │   │
│   │    ├── Network Proxy Service                                   │   │
│   │    └── Virtualization.framework                                │   │
│   └────────────────────────────────────────────────────────────────┘   │
│                                    │                                   │
│         ┌──────────────────────────┼──────────────────────────┐        │
│         │  stdio    VirtioFS    MCP SDK    HTTP/SOCKS         │        │
│         │  pipes    mounts      protocol   proxy              │        │
│         └──────────────────────────┼──────────────────────────┘        │
│                                    ▼                                   │
│   ┌────────────────────────────────────────────────────────────────┐   │
│   │  Ubuntu 22.04 VM (ARM64)                                       │   │
│   │                                                                │   │
│   │    /sessions/<name>/mnt/ ◄── VirtioFS ──► ~/<shared-folder>/   │   │
│   │                                                                │   │
│   │    ┌────────────────────────────────────────────────────────┐  │   │
│   │    │  bubblewrap sandbox + seccomp (per session)            │  │   │
│   │    │                                                        │  │   │
│   │    │    Claude Code CLI v2.1.15                             │  │   │
│   │    │      --model claude-opus-4-5-20251101                  │  │   │
│   │    │      --mcp-config {...}                                │  │   │
│   │    │      --allowedTools Task,Bash,Grep,...                 │  │   │
│   │    │                                                        │  │   │
│   │    └────────────────────────────────────────────────────────┘  │   │
│   │                                                                │   │
│   │    Network: :3128 (HTTP) / :1080 (SOCKS) → Allowlist           │   │
│   │    Disks: rootfs.img (10GB) + sessiondata.img (36MB)           │   │
│   │                                                                │   │
│   └────────────────────────────────────────────────────────────────┘   │
│                                                                        │
└────────────────────────────────────────────────────────────────────────┘

Claude Cowork is not a separate model; it is Claude Code CLI running inside the VM, orchestrated by Claude Desktop:

Component Value
CLI Binary /usr/local/bin/claude (ELF 64-bit ARM aarch64)
CLI Version 2.1.15 (Claude Code)
Model claude-opus-4-5-20251101
I/O Format stream-json (bidirectional)
Session Resume --resume <session-uuid>

The CLI is launched with the following arguments:

/usr/local/bin/claude \
  --output-format stream-json \
  --input-format stream-json \
  --model claude-opus-4-5-20251101 \
  --resume <session-uuid> \
  --allowedTools Task,Bash,Glob,Grep,Read,Edit,Write,... \
  --mcp-config '{"mcpServers": {...}}' \
  --permission-mode default \
  --plugin-dir /sessions/<name>/mnt/.skills

Isolation

Virtual Machine

The execution environment is a complete Ubuntu 22.04 LTS virtual machine running on ARM64 architecture:

PRETTY_NAME="Ubuntu 22.04.5 LTS"
Linux claude 6.8.0-90-generic aarch64 GNU/Linux

This is not a Docker container or a lightweight sandbox; it is a full VM with its own kernel, managed by Apple’s Virtualization Framework. The VM runs via com.apple.Virtualization.VirtualMachine, the same technology used by tools like UTM and Tart.

Resources allocated:

  • 4 vCPUs (ARM64)
  • 3.8 GB RAM
  • ~10 GB virtual disk (sparse)

VM Bundle Structure

The VM files are stored locally at:

~/Library/Application Support/Claude/vm_bundles/claudevm.bundle/
File Size Purpose
rootfs.img 10 GB Ubuntu root filesystem (sparse)
sessiondata.img 36 MB Persistent session data (/sessions)
efivars.fd 128 KB UEFI boot variables
macAddress 17 B Virtual MAC address
machineIdentifier 70 B Unique VM identifier
.rootfs.img.origin 40 B SHA-1 hash for image verification

The rootfs.img is a sparse file, meaning it does not actually consume 10 GB on disk; it uses only the space needed for actual data.

Security Layers

The VM employs multiple security layers:

┌──────────────────────────────────────────────┐
│  macOS Host                                  │
│    └── Apple Virtualization Framework        │
│          └── Ubuntu 22.04 VM                 │
│                └── bubblewrap sandbox        │
│                      └── seccomp filter      │
│                            └── Claude Code   │
└──────────────────────────────────────────────┘
  • VM isolation: Full hardware-level separation
  • bubblewrap (bwrap): Linux sandboxing tool
  • seccomp: System call filtering
  • Network isolation: Traffic routed through local proxy (ports 3128/1080)

Multi-Session Architecture

A single VM instance serves multiple Cowork conversations simultaneously. Each conversation gets its own isolated session:

/sessions/
├── intelligent-loving-darwin/  ← Chat session 1
├── dreamy-optimistic-babbage/  ← Chat session 2
└── ...

Session names are randomly generated using a Docker-container-like pattern: adjective-adjective-scientist.

Resource Shared? Notes
/tmp/ Yes Sessions can see each other’s temporary files
/sessions/<name>/ No Permissions drwxr-x--- block cross-session access
User (UID) No Each active session has its own Linux user (e.g., uid=1002)
Kernel/Processes Yes Same VM, same kernel
Network Proxy Yes Same allowlist rules

This was verified by creating a file in /tmp/ from one session and successfully reading it from another, whereas an attempt to run ls /sessions/dreamy-optimistic-babbage/ returned Permission denied.

Interestingly, inactive sessions show nobody:nogroup as owner, while the active session has its own dedicated user. This suggests sessions are “depersonalized” when closed.

File Sharing

User folders are shared between macOS and the VM using VirtioFS, Apple’s high-performance paravirtualized filesystem:

/mnt/.virtiofs-root/shared/Downloads → /sessions/.../mnt/Downloads

This enables bidirectional file access: Claude can read and write to the user’s selected folder, and changes appear instantly on both sides.

Path Translation

Claude Desktop performs smart path translation in the UI. When Claude executes:

cp report.pdf /sessions/intelligent-loving-darwin/mnt/Downloads/

The user sees:

cp report.pdf ~/Downloads/

This translation is context-aware: it only rewrites paths that are actual file arguments, not string literals in other commands.

Networking

Allowlist

All network traffic from the VM is routed through a local proxy with a strict allowlist. Direct DNS lookups are blocked (socket(): Operation not permitted).

Allowed Domains:

Domain Status Purpose
api.anthropic.com 200 Anthropic API
pypi.org 200 Python packages
registry.npmjs.org 200 Node packages

Blocked Domains:

Domain Response
google.com 403 Forbidden - blocked-by-allowlist
api.github.com 403 Forbidden - blocked-by-allowlist
Any Other Domain 403 Forbidden - blocked-by-allowlist

This means Claude can install dependencies via pip and npm, but cannot make arbitrary HTTP requests from the VM—curl to non-allowlisted domains is blocked, and the same applies to the WebFetch tool. However, WebSearch works as it uses the Anthropic API endpoint.

Host-VM Communication

Communication between Claude Desktop (macOS) and Claude Code (VM) uses multiple channels:

┌─────────────────────────────────────────────────────────────────┐
│  macOS (Claude Desktop)                                         │
│                                                                 │
│    ┌─────────────────┐                                          │
│    │  Electron App   │                                          │
│    │                 │                                          │
│    │  stdio pipes ───┼──────────────────────┐                   │
│    │                 │                      │                   │
│    │  Unix sockets ──┼───┐                  │                   │
│    └─────────────────┘   │                  │                   │
│                          │                  │                   │
└──────────────────────────│──────────────────│───────────────────┘
                           │ virtiofs mount   │ pipes
┌──────────────────────────│──────────────────│───────────────────┐
│  VM                      │                  │                   │
│                          ▼                  ▼                   │
│    /tmp/claude-http-*.sock     pipe:[xxxxx] (stdin)             │
│    /tmp/claude-socks-*.sock    pipe:[xxxxx] (stdout)            │
│           │                         │                           │
│           ▼                         ▼                           │
│    socat → localhost:3128    Claude Code CLI                    │
│    socat → localhost:1080      --input-format stream-json       │
│    (HTTP/SOCKS proxy)          --output-format stream-json      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
Channel Mechanism Purpose
Messages Unix Pipes (stdin/stdout) JSON stream for user messages and responses
HTTP Proxy Unix Socket → socat → :3128 HTTP requests (pip, npm, API calls)
SOCKS Proxy Unix Socket → socat → :1080 Other network protocols
MCP SDK Protocol via Pipes Communication with MCP servers on host
Files VirtioFS Mount Shared folder access

The Unix sockets (/tmp/claude-*.sock) are mounted from macOS into the VM via VirtioFS, allowing socat processes inside the VM to bridge network traffic to the host’s proxy.

MCP

One of the most interesting aspects of the architecture is how Model Context Protocol (MCP) servers are integrated. Claude Desktop’s MCP servers are passed to the VM and made available to Claude Code. The MCP configuration is injected via a command-line argument:

{
  "mcpServers": {
    "ea93ae0e-73b4-4d43-9bb0-2c3720b9d627": {"type": "sdk", "name": "..."},
    "4b26c136-8d30-4046-b6ad-2e41dde789ea": {"type": "sdk", "name": "..."},
    "Claude in Chrome": {"type": "sdk", "name": "Claude in Chrome"},
    "My Custom Server": {"type": "sdk", "name": "My Custom Server"},
    "cowork": {"type": "sdk", "name": "cowork"}
  }
}

Official Anthropic integrations use UUIDs as identifiers (e.g., ea93ae0e-... for Atlassian, 4b26c136-... for Slack), while third-party and custom servers use human-readable names. The cowork server is built-in and provides tools like request_cowork_directory and allow_cowork_file_delete.

MCP servers configured in Claude Desktop are dynamically passed through to the VM. When a new MCP server is added to Claude Desktop, it becomes available to Cowork sessions. MCP servers use "type": "sdk", which means they communicate through the Claude SDK rather than stdio pipes, enabling Claude inside the VM to interact with applications running on the host.

Preinstalled Tools

The VM comes with a comprehensive toolkit:

Languages:

Language Version
Python 3.10.12
Node.js 22.22.0
Ruby 3.0.2
TypeScript 5.9.3

CLI Tools:

  • ffmpeg/ffprobe 4.4.2 — Video/audio processing
  • git 2.34.1 — Version control
  • pandoc 2.9.2 — Document conversion
  • ImageMagick 6.9.11 — Image manipulation
  • ripgrep — Fast search
  • jq 1.6 — JSON processing
  • sqlite3 — Database

Node.js Packages:

  • docx — Word document creation
  • pptxgenjs — PowerPoint generation
  • pdf-lib — PDF manipulation
  • sharp — Image processing

Python Packages:

  • beautifulsoup4 — Web scraping
  • camelot-py — PDF table extraction
  • pandas, numpy, matplotlib — Data analysis

Security Implications

The architecture provides strong isolation:

  1. No direct host access: Claude cannot access arbitrary files on macOS, only the explicitly shared folder
  2. Network allowlist: Only package registries (pypi, npm) and Anthropic’s API are accessible; arbitrary web requests are blocked
  3. Syscall restrictions: seccomp limits what system calls can be made
  4. Per-session sandboxing: Each conversation runs in its own bubblewrap sandbox with a dedicated user
  5. DNS blocked: Direct DNS lookups fail; all traffic must go through the proxy

Potential Concerns:

  • Sessions sharing /tmp/ could theoretically leak information between conversations
  • The VM persists between sessions, so artifacts may remain in shared spaces

This is a significant improvement over running code directly on the host or in a simple container.

Conclusion

Claude Cowork represents a thoughtful approach to enabling AI code execution. By running a full Linux VM with multiple security layers, Anthropic has created an environment that is both powerful (full Ubuntu with extensive tooling) and isolated (VM + sandbox + seccomp + network allowlist).

Key architectural decisions:

  1. Claude Code as the engine: Cowork is Claude Code CLI running inside a sandboxed VM, not a separate product
  2. Single VM, multiple sessions: Efficient resource usage while maintaining per-session isolation
  3. Apple Virtualization Framework: Native ARM64 performance on Apple Silicon
  4. VirtioFS: Fast, bidirectional file sharing with the host
  5. MCP passthrough: Desktop MCP servers are dynamically shared with the VM via SDK protocol
  6. Network allowlist: Permits dependency installation while blocking arbitrary web access
  7. Smart path translation: Seamless UX by rewriting VM paths to host paths in the UI

This architecture allows Claude to perform complex tasks—video processing, document generation, data analysis—while maintaining strong security boundaries. It is a practical solution to the challenge of giving AI systems the ability to execute code safely.

CVE-2024-40801: How a Sandboxed Mac App Could Steal Your Private Data Bypassing TCC Protections

EN

This post includes the details of the first vulnerability I have ever reported to Apple. It was fixed in macOS Sonoma 14.7 and macOS Sequoia 15.0 as CVE-2024-40801.

TL;DR

A vulnerability in macOS allowed a sandboxed app to bypass TCC (Transparency, Consent, and Control) protections and access sensitive user data without requiring user permission. By leveraging the container-migration.plist feature, a sandboxed app could request the migration of TCC-protected files (like Safari history, the Mail database, or user documents) to its app container, effectively bypassing TCC and giving the app full access to these files. There are multiple examples included in this repository demonstrating the exploit.

Initial Report

Below is the full report as submitted to Apple. You can also check the GitHub repository that includes the example projects to reproduce the vulnerability.

Introduction

A sandboxed Mac app can exploit the container-migration.plist feature to gain access to TCC-protected files without any user permission prompt.

For example, you can request the Safari history file or Mail database to be migrated by the App Sandbox to the app container, and it will happily do it. Once the files are in the app container, the app has full control to read and exfiltrate this data.

You can use the attached project to reproduce the exploit (check the demo video at Extra/Videos/ContainerMigrationExploit.mp4):

Steps to Reproduce

  1. Run the script at Scripts/ContainerMigrationExploitReset.sh in Terminal with Full Disk Access. This script will:
    • Reset the App Sandbox container of the Exploit app (if it exists).
    • Reset the TCC permissions of the Expected app that shows the proper expected behavior when accessing the protected files.
    • Create a demo file in the user Documents folder named my-secret.txt.
    • Restore and back up the Safari history database (History.db) and Mail recent searches plist (recentSearches.plist). Both of these files are protected by TCC, and reading them requires Full Disk Access as they contain very sensitive data like contacts and the browsing history. The first time the script runs, the restore will fail, but you can ignore it.
  2. Open the Xcode project at Projects/ContainerMigrationExploit.xcodeproj.
  3. Run the Expected scheme: this is a non-sandboxed app that tries to read directly the files that the exploit app will steal. As you can see, it triggers the expected TCC permission prompt when reading the my-secret.txt file in the Documents folder, and it also cannot access the Safari history database or the Mail recent searches plist, as they are stored in protected directories.
  4. Now run the Exploit scheme: this sandboxed app is able to read the three files without any issues, as they have been migrated by the App Sandbox into the app container.

Expected Results

As demonstrated by the Expected scheme, the app should not be able to access any of the data in the protected directories without user permissions and/or Full Disk Access. Even worse, a sandboxed app is able to gain more access to sensitive files than a non-sandboxed app using this technique.

Actual Results

The Exploit app can access sensitive files protected by TCC without any user permission. This same technique can be used to exfiltrate the following data from a fully sandboxed app:

  • User documents stored in the Documents folder (without any TCC prompt).
  • Sensitive files in the Library folder:
    • Safari history & bookmarks.
    • Full Mail database & contacts.
    • Other apps’ containers’ data.

Annex I

This new version of the project includes a new Mail-app-specific example project in Projects/MailContactsExploit.xcodeproj that demonstrates how you can use this exploit to dump all your Mail contact addresses without any TCC prompt from a fully sandboxed app (demo video at Extra/Videos/MailContactsExploit.mp4).

This same exploit can also be used for a denial-of-service / ransomware attack, as the original files (in this example, the Mail database) are deleted by the App Sandbox migration from the original location and are now under the full control of the attacker app.

Annex II

Some details about how the exploit seems to work under the hood:

  1. The sandboxed app initializes the App Sandbox and connects to the secinitd daemon.
  2. secinitd reads the container-migration.plist file in the app bundle.
  3. As secinitd has the kTCCServiceSystemPolicyAllFiles value in the com.apple.private.tcc.allow entitlement, it can access any protected directory and moves the protected files into the app container.

You can check the related Endpoint Security events from eslogger in the directory Data/EndpointSecurity/.

Annex III

This final version of the project includes a demonstration that this same exploit can also be used to exfiltrate both Calendar and Contacts databases, even though their paths are symlinked inside the app container, by leveraging a custom destination in the container-migration.plist file:

<dict>
    <key>Move</key>
    <array>
        <array>
            <string>${Home}/Library/Calendars/Calendar.sqlitedb</string>
            <string>${Home}/Calendar.sqlitedb</string>
        </array>
    </array>
</dict>

You can check a Calendar-specific example project at Projects/CalendarExploit.xcodeproj and a demo video at Extra/Videos/CalendarExploit.mp4.