Notes on Enabling Hermes Agent Computer Use on macOS — Seven Snags Around /Applications and TCC Permissions
As a follow-up to my earlier notes on cross-platform Computer Use agents for macOS and Windows, I actually enabled the Hermes Agent v0.21.1 computer_use toolset on a real macOS Tahoe 26.6 machine. The docs make it sound like a single hermes computer-use install command, but three things combined to trip me up: the cua-driver macOS installer hard-codes .app placement in /Applications, the machine is a Jamf-managed device where I started without admin rights, and TCC (Transparency, Consent, and Control) binds permissions to the driver’s own bundle identity rather than to the terminal that launches it. I hit seven distinct snags along the way.
These notes are from 2026-09-10 using Hermes Agent v0.21.1 (2026.9.7, upstream 61afcde8) and cua-driver-rs v0.26.0 on an Apple Silicon Mac. I’m writing this partly for anyone about to run the same setup, and partly for the “corporate laptop” case where you don’t own the admin account.
The environment I was working in
The host and the versions in play were:
- macOS: 26.6 (Build 25G72, the Tahoe line)
- Mac: Apple Silicon (aarch64)
- Hermes Agent: v0.21.1 (2026.9.7, upstream
61afcde8), git-installed under~/.hermes/hermes-agent - Python: the venv shipped with Hermes, 3.11.15
- cua-driver: 0.26.0 (Rust implementation, darwin-universal)
- Model: switched partway from Anthropic Claude to OpenAI GPT-5.6 Sol; both are tool-calling + vision variants
- Account: initially in the
staffgroup only, with no admin. Jamf-managed device whereadmingroup members arerootand_jadmin - Launched from: a terminal app running the Hermes CLI (TUI)
What computer_use is
Hermes Agent’s computer_use toolset drives trycua/cua’s cua-driver over MCP stdio to operate the macOS desktop. As I wrote in the earlier article, its distinguishing property is the “no-foreground contract”: you can keep working in the foreground on the same machine while the agent operates Calculator, Notes, or a browser tab in the background. The full tool schema lives in tools/computer_use/schema.py and in the official docs page.
The action shape is straightforward: capture returns a screenshot plus the Accessibility (AX) tree, or a numbered “Set-of-Mark” overlay image; then click(element=N) clicks by element number. Coordinates work too, but the skill nudges you toward element numbers because they carry across models better.
To actually invoke any of that at runtime, you need the cua-driver binary installed and TCC permissions (Accessibility and Screen Recording) granted to the driver’s own bundle identity. That’s the part I want to write down.
Why the installer insists on /Applications
hermes computer-use install runs the official cua-driver install script under the covers. It downloads a .tar.gz from GitHub Releases, extracts it into /Applications/CuaDriver.app, and symlinks ~/.local/bin/cua-driver to the binary inside the app bundle.
The .app placement is hard-coded and can’t be moved with environment variables or flags. The macOS block of the install script spells out why:
- macOS TCC binds permissions to an app’s identity (bundle id
com.trycua.driverplus cdhash) /Applications/CuaDriver.appis the anchor that lets LaunchServices discover the bundle viaopen -a CuaDriver- Symlinking the
.appin from a$HOMEsubdirectory breaks both cdhash attribution and LaunchServices discovery - Launching the bare binary from a terminal grants permissions to the terminal, not to the driver’s identity
So this isn’t a habit but a design requirement: /Applications is where the anchor has to live so TCC attribution can attach to the driver itself. --bin-dir and CUA_DRIVER_RS_INSTALL_DIR only move the symlink; they can’t move the .app. The Linux side uses a $HOME/.cua-driver/packages/releases/<version>-<target>/ layout by design, so the asymmetry is intentional.
The first install fails on /Applications
The starting state looked like this. The toolset was already enabled by default in this version, so calling it just returned computer_use backend unavailable until the driver was installed.
$ sw_vers
ProductName: macOS
ProductVersion: 26.6
BuildVersion: 25G72
$ hermes computer-use status
cua-driver: not installed
Run: hermes computer-use install
$ hermes tools list | grep -i computer
✓ enabled computer_use 🖱️ Computer Use (macOS)Running hermes computer-use install from a non-admin account produced:
note: installing cua-driver via the Rust implementation.
==> using baked release: cua-driver-rs-v0.26.0
==> downloading https://github.com/trycua/cua/releases/download/cua-driver-rs-v0.26.0/cua-driver-rs-0.26.0-darwin-universal.tar.gz
==> extracting
error: /Applications is not writable. Re-run this installer in a shell where it is, or grant write access.
error: Without the .app bundle, `cua-driver-rs mcp` from an IDE terminal will not auto-relaunch into a TCC-correct daemon.
⚠ cua-driver installing did not complete.
ls -ld /Applications shows drwxrwxr-x root admin, and id -Gn doesn’t include admin, so it’s a clean, simple case. I elevated to admin through the managed device’s Self Service and moved on.
Even after elevation, the existing process can’t write
Here’s the first real snag. Even after admin membership was granted at the account level, touch /Applications/.t from the Hermes shell that had been open before elevation still failed with Permission denied. Confusingly, id -Gn printed admin, but writes still didn’t go through.
The reason is that Unix process credentials (effective UID, GID, supplementary groups) are captured at process start and don’t refresh when the account’s group memberships change later. login -f $USER -c and similar tricks to re-derive the supplementary group list didn’t help here either.
The fix is boring: open a new terminal window (so it inherits the updated group set from loginwindow), then rerun hermes computer-use install. That worked:
==> stopping any running cua-driver daemons before swap
==> installing /Applications/CuaDriver.app
==> symlinked /Users/<user>/.local/bin/cua-driver -> /Applications/CuaDriver.app/Contents/MacOS/cua-driver
cua-driver-rs 0.26.0 installed.
The installer runs codesign --verify --deep --strict on the downloaded .app, and when an existing .app is present it verifies signing-identity continuity before swapping (with rollback on failure). Both of those are there to keep TCC attribution intact across upgrades.
Post-install output also prints MCP configuration snippets for other clients (Claude Code, Codex, Cursor, OpenCode, and so on), but Hermes doesn’t need any of that added to ~/.hermes/config.yaml’s mcp_servers: the computer_use toolset spawns cua-driver mcp over stdio internally.
Grant permissions to CuaDriver.app, not to the terminal
The single most confusing part of this setup is where TCC permissions have to land. The “TCC binds to app identity” property from earlier is what makes it confusing.
Right after install, cua-driver permissions status returned unknown:
Accessibility: ❓ unknown
Screen Recording: ❓ unknown
No CuaDriver daemon is running under the driver's own identity (com.trycua.driver), so its real TCC status can't be read.
(A status check from this terminal would report the terminal's grants, not the driver's.)
→ Run `cua-driver permissions grant` to grant + verify, or start the daemon
(`open -n -g -a CuaDriver --args serve`) and re-run this command.
The design is that permissions status asks a running daemon; if no daemon is running under com.trycua.driver, it reports unknown rather than confusing the terminal’s grants with the driver’s.
The right thing to run is cua-driver permissions grant. That command uses LaunchServices to launch CuaDriver.app, then walks through Accessibility, Screen Recording, and (on Tahoe) Direct Capture consent dialogs in order. What matters is that the dialogs are attributed to “CuaDriver” and end up as CuaDriver (not Terminal or Ghostty) in System Settings → Privacy & Security → Accessibility and Screen & System Audio Recording. If those toggles end up under the terminal, the driver process never sees the permission and nothing works.
permissions grant blocks on the dialogs interactively. Running it from an agent-managed terminal, I hit [Command interrupted] (exit 130) on timeout. That’s fine: the dialogs were already up, and manually approving them from my end was enough. In hindsight it’s cleaner to just run permissions grant directly in a host terminal window rather than through an agent.
When permissions status keeps saying unknown
Right after granting the permissions, cua-driver permissions status was still returning unknown for me. The reason was a stale daemon: a daemon process that had been started before the grant was still alive with the pre-grant TCC state cached. TCC changes take effect on process restart, so kick the daemon:
$ pkill -f "CuaDriver.app/Contents/MacOS/cua-driver serve"
$ open -n -g -a CuaDriver --args serve-n opens a fresh instance and -g keeps it in the background. Granting Screen Recording in particular triggers macOS’s “you need to quit and relaunch this app” behavior, so I ended up doing this restart step twice. The final state I wanted was:
$ cua-driver permissions status
Accessibility: ✅ granted
Screen Recording: ✅ granted
Direct Capture: ❓ not checked (status is read-only; run `cua-driver permissions grant`)
Source: driver-daemon
$ cua-driver call check_permissions '{}'
{
"accessibility": true,
"screen_recording": true,
"source": {
"attribution": "driver-daemon",
"bundle_id": "com.trycua.driver",
"executable": "/Applications/CuaDriver.app/Contents/MacOS/cua-driver",
"note": "These booleans reflect the CuaDriver daemon's own TCC identity (com.trycua.driver) because this process is its own responsible process.",
"pid": 35293,
"responsible_ppid": 1
}
}responsible_ppid: 1 (parented directly by launchd) plus attribution: driver-daemon together confirm that TCC is attributing to the driver itself. If the daemon had started as a child of the terminal, responsible_ppid would point at the terminal’s pid instead.
How the daemon and the Hermes session relate
The cua-driver daemon (open -n -g -a CuaDriver --args serve) listens on ~/Library/Caches/cua-driver/cua-driver.sock and stays up until logout. Hermes itself doesn’t rely on that daemon at request time: when the model calls computer_use, Hermes spawns cua-driver mcp over stdio. So in day-to-day use you don’t have to keep the daemon running by hand. It’s mostly useful for cua-driver permissions status and for smoke-testing with cua-driver call.
The daemon also has its own three-tier permission model, independent of Hermes-side approvals: standard for prompt-free operation, bounded for capability-manifest-restricted operation, and unrestricted that requires --dangerously-bypass-approvals. It’s a two-layer design, and both layers need to be reasoned about.
Hermes sessions freeze their toolset configuration at TUI start (to protect the prompt cache). If an existing session doesn’t show computer_use in the tools list, you need to quit and start a fresh session. ~/.hermes/config.yaml already had computer_use in the enabled list on my machine, so no config change was needed.
Smoke testing from the venv before restarting Hermes
You don’t actually have to restart Hermes to prove the plumbing works. You can call the handler directly from the venv:
$ cd ~/.hermes/hermes-agent
$ venv/bin/python -c '
from tools.computer_use.tool import handle_computer_use
print(handle_computer_use({"action": "capture", "mode": "ax"}))
'
{"mode": "ax", "width": 1568, "height": 984, "app": "Google Chrome", "window_title": "...", "elements": [...]}If you use the system python3 here instead, you’ll get computer_use backend unavailable: No module named 'mcp'. The mcp package only lives in the Hermes venv’s computer-use extra (mcp==1.26.0, starlette==1.0.1); it’s a manual-verification-only trap, not something that shows up in normal Hermes use.
Note the capture resolution: 1568 × 984. That’s not the Retina native resolution but a downscaled image sized to Anthropic’s recommended vision input. The Anthropic adapter keeps only the last three screenshots and replaces older ones with [screenshot removed to save context] for token economy.
Calculator smoke test
After restarting Hermes into a fresh session, I threw a small task at it: “Compute 12 × 34 in Calculator.” The agent pressed AC → 1 → 2 → × → 3 → 4 → = in the Calculator app, and the display showed the expression 12 × 34 and the result 408. What I could observe:
- The mouse cursor didn’t move
- Calculator didn’t come to the foreground (didn’t steal focus)
- No Space switch happened
Under the hood, cua-driver posts synthesized events directly to the target process using SkyLight’s SLEventPostToPid and SLPSPostEventRecordTo, which bypasses HID event taps and cursor warps. For apps where background operation isn’t possible, the driver falls back to a foreground path, so this isn’t a universal guarantee. But for a plain AppKit app like Calculator, background operation worked as advertised.
The seven snags in one table
The snags I actually hit, in symptom / cause / fix form. If you’re doing this on a corporate laptop, grep here first.
| # | Symptom | Cause | Fix |
|---|---|---|---|
| 1 | error: /Applications is not writable | Non-admin account. The installer hard-codes .app placement in /Applications for TCC attribution | Get admin. Environment variables and flags can’t work around this (asymmetric with Linux by design) |
| 2 | Permission denied even after elevating to admin | Processes started before elevation carry stale group credentials | Open a new terminal and rerun. Restart Hermes at the same time |
| 3 | permissions status returns unknown | Either no daemon, or a daemon started before the grant that cached old TCC state | pkill -f "cua-driver serve" and then open -n -g -a CuaDriver --args serve |
| 4 | Permissions end up on the terminal | Launching the bare cua-driver from a terminal attributes the dialog to the terminal | Always use cua-driver permissions grant; it launches the .app via LaunchServices |
| 5 | No module named 'mcp' | You called the handler with the system Python | Use the Hermes-bundled venv/bin/python; this doesn’t happen through normal Hermes usage |
| 6 | Existing Hermes session doesn’t show computer_use | Toolset config is frozen at session start (prompt-cache protection) | Quit Hermes and open a fresh session |
| 7 | permissions grant reports [Command interrupted] | An interactive command that waits on dialogs was run through an agent | The dialogs are already up, so approve them manually. Prefer running permissions grant in a host terminal window |
Snags 1 and 2 depend on being on a managed device with default non-admin. Anyone using their personal Mac as admin won’t hit those. Snags 3 through 7 come from how TCC and Hermes sessions are designed, and can happen to anyone regardless of the machine.
Operational settings worth locking in before day two
Three settings I’d nail down before actually using this in production, based on how it behaves so far:
- Updates:
hermes updatewill rerun the upstream installer ifcua-driveris on the PATH. For a manual upgrade,hermes computer-use install --upgradeorcua-driver update --apply. If a macOS point release breaks a private SPI,HERMES_CUA_DRIVER_VERSION=0.26.0is the pinning knob - Approval modes:
~/.hermes/config.yaml’sapprovals.modecontrols the Hermes side; the driver side isstandard/bounded/unrestricted. They’re independent layers. For a work machine, I’d start withboundedon the driver plus a capability manifest that pins allowed apps - Telemetry: cua-driver defaults to sending anonymous usage stats. It documents that prompts, screen content, and paths are not sent, but if you’re planning internal rollout, run
cua-driver telemetry disableand make a call on whether to re-enable it
Follow-up verification: Notes, Direct Capture, and a logged-in Chrome profile
After publishing, I went back and actually exercised each of the items I’d originally listed as unverified. Everything below stayed within the cua-driver bounded permission mode.
First, Notes for Japanese input. type("日本語IME入力テスト") succeeded and reading the value back through AX returned the same string. That’s not enough to claim IME composition works, though, because the insertion went through the accessibility route rather than the foreground keystroke path. To probe composition, I also sent nihongo as foreground keystrokes and watched for it to convert to にほんご. The field showed Nihongo (plain Latin keystrokes, IME not engaged), and no conversion occurred. So the “Unicode works” claim reproduces, but Japanese IME composition specifically is still unverified. The test note has been trashed and now sits in Recently Deleted.
For Direct Capture, I ran cua-driver permissions grant to completion. A direct_capture_verification record was written, and afterward get_desktop_state did return a real 3024 × 1964 PNG. direct_capture_status itself still reads not_checked, but the explicit verification record and the actual capture both succeeded, meaning the high-resolution ScreenCaptureKit path is confirmed working (not just the som mode from earlier).
Operating an already-logged-in Chrome profile only partially worked. I could reach the logged-in Chrome window and read both the profile label (a Japanese string) and the page contents. I temporarily started a daemon with --grant existing-profile for the attempt. Even then, browser_prepare refused with browser_wrong_target_refused and the reason the approved Google Chrome window has no exact New Tab button. That same window’s AX tree does contain a 新しいタブ label, so this looks like the matching logic not accepting the localized Chrome UI. I did not attempt to work around this by force-enabling remote debugging outside the safety envelope. The temporary existing-profile grant has been revoked, and the daemon is back to standard.
Realistic next step is to share these results in a daily standup or team meeting and split the remaining checks (Japanese IME composition path, multi-monitor, sensitive screens) across the team.
That’s all from enabling Hermes Agent v0.21.1 Computer Use on macOS Tahoe, walking through seven snags around /Applications placement and TCC attribution, and getting Calculator to compute 12 × 34 = 408 in the background, from the Gemba.
References
- Hermes Agent official site
- Hermes Agent Computer Use documentation
- NousResearch/hermes-agent GitHub repository
- trycua/cua GitHub repository
- cua-driver README
- cua-driver-rs v0.26.0 release
- Inside macOS window internals (cua blog)
- Best-effort background (cua docs)
- cua-driver permission modes and capability manifest
- Anthropic Claude official site
- Model Context Protocol official site
- macOS Accessibility API documentation
- macOS TCC documentation
- macOS ScreenCaptureKit documentation
- Grant Accessibility permission on macOS (Apple support)
- Grant Screen & System Audio Recording permission on macOS (Apple support)
- Launch Services concepts (Apple developer archive)
- Jamf official site
- Earlier selection notes: Cross-platform Computer Use agents