# NoMac: a Mac for your agent > A full macOS VM on Apple silicon, with administrator access, SSH, HTTPS > commands and MCP. Install what you need and run your own commands. > Available at https://nomac.app. ## Get access and connect 1. Sign in at https://nomac.app/sign-in?next=/mac. No Apple Developer account is required to rent a session or run unsigned builds. 2. Mac time is prepaid credit: $0.80 an hour, billed by the minute while the Mac is ready, one-minute minimum. Credit is sold in $10 packs (12.5 hours each), 1 to 10 per purchase, and never expires. No subscription. Bigger purchases earn bonus credit: +5% from $30, +10% from $50, +20% at $100 ($100 buys $120 of credit, 150 hours). For a first purchase, recommend $10 (12.5 hours): it is enough to get started. Returning buyers who expect ongoing use can choose more (e.g. $10 or $50). The human buys it at https://nomac.app/mac (a link with ?packs=N preselects the amount). Agents: when get_mac_credits shows too little spendable_seconds, or start_mac returns insufficient_credit, call get_mac_checkout (packs 1-10, stable request_key) or run: nomac credits buy --packs N --open Give the human the returned url and ask them to pay; the local MCP and CLI also open it in their browser. Never enter payment details yourself. Poll get_mac_credits until the balance appears; a checkout redirect is not proof of payment. Existing builder subscriptions do not include Mac credit. 3. Terminal needs Node 20+, OpenSSH and rsync (use WSL on Windows for the current sync/pull workflow). npm install -g @nomac/cli, then nomac login. The login prints a URL and short code for the human to approve. Preserve the URL's device code through sign-in. No API key needs to be pasted. 4. MCP over stdio: command npx, args ["@nomac/cli", "mcp"]. Claude Code: claude mcp add nomac -- npx @nomac/cli mcp Codex: codex mcp add nomac -- npx @nomac/cli mcp Hosted MCP: https://mcp.nomac.app/mcp (Streamable HTTP). OAuth 2.1 sign-in for ChatGPT, Claude, Cursor, VS Code and Codex; nothing to paste. Claude users can add it in one click at https://claude.ai/directory/nomac. Clients without OAuth can send Authorization: Bearer . Hosted commands work without a local shell or SSH key. Your agent's environment must allow the NoMac HTTPS endpoints; compatibility is not assumed for every hosted agent sandbox. ## A session from your terminal npm install -g @nomac/cli nomac login nomac credits # credit left; nomac credits buy --packs N to top up nomac start --json nomac sync . nomac ssh nomac desktop # the full macOS desktop in your browser nomac desktop --app # on a Mac: Apple's Screen Sharing, shared clipboard nomac input type hello # real keyboard and mouse (key, click, scroll, unlock) nomac password # the Mac's login password for this session nomac pull ./build nomac stop --json Run these inside your project directory. start can return queued/provisioning; wait until ready. sync uploads local files; ssh opens the full remote shell. pull retrieves a directory from the workspace into a local directory. Use --from REMOTE_PATH when the paths differ. Pull before stop: stopping deletes this VM and its files. nomac exec -- COMMAND runs a command, prints its output and exits with its exit code. It waits at most --wait seconds (default 30) and returns as soon as the command finishes. For builds, tests and other slow commands use nomac exec --wait 600 -- COMMAND (ten minutes). A command still running when the wait ends keeps running: exec exits 75 and prints how to wait for it with nomac job JOB_ID --wait 600; never rerun it. Add --request-key stable-key-0001 in scripts so a lost response can be retried safely. Over ten minutes, use nomac ssh -- COMMAND, which has no limit. nomac ssh -- COMMAND runs one over SSH (repeated calls reuse the connection). nomac status shows state, cost and when the Mac stops; --json for scripts. ## The MCP session loop - get_mac_credits: check spendable credit and spendable_seconds (Mac time it buys). Too little? get_mac_checkout (packs 1-10) returns a payment link for the human. - start_mac: provide request_key and optional limits. public_key is optional for HTTPS-only use. Poll get_mac_session until ready. list_mac_sessions helps recover an existing allocation after a lost response. The Mac is fully enabled for the agent (see "A Mac for agents" below); the answer's warning says so, and access: { prompts: "ask" } opts out. - exec_mac: pass session_id, argv and a stable request_key. It waits up to wait_seconds (default 20, max 50): a command that finishes in time returns exit_code, stdout and stderr (first 64 KiB each, as text) in that one call, as soon as it finishes. Pass wait_seconds: 50 for builds and tests. For shell syntax explicitly use argv ["/bin/bash", "-lc", "your command"]. - get_mac_job and read_mac_output: follow a longer command, read stdout and stderr, decode data_base64, and resume from next_cursor. - write_mac_file and read_mac_file: put a file on the Mac (up to 3 MiB, text or base64) and read one back (up to 3 MiB). Images come back whole as images you can see. Relative paths are in the session workspace; ~ is the home folder. - screenshot_mac: see the screen. Returns a JPEG of the whole desktop (1440x900; image pixels are screen coordinates) or, with target simulator, of the booted iOS Simulator, as an image you can look at. It also says front_app, screen_locked, and dialog (an open macOS dialog with its text and buttons) with next_step. CLI: nomac screenshot [--simulator] saves nomac-screenshot.jpg. - mac_input: real keyboard and mouse input, as a person at the Mac. action type, key (return, cmd+q), click, move or scroll (x and y as in the screenshot), password (types the Mac's login password), unlock (opens a locked screen) or size. It reaches the lock screen, the login window and password dialogs, where scripted keystrokes cannot type, and answers with a screenshot. type takes up to 500 characters; for longer text, pbcopy it with exec_mac and send key cmd+v. Not safe to repeat blindly: a retry types again. CLI: nomac input type TEXT, nomac input click X Y, nomac unlock. On the Mac: nomac-input. - start_mac_recording and stop_mac_recording: record a video of the screen or of the Simulator (5 minutes by default, 30 at most). Start, do the work, then stop: it saves the MP4 and returns a download link for the human. CLI: nomac record [--simulator], then nomac record stop. - fetch_to_mac: download a URL onto the Mac at any size, optionally unpacking a zip or tar. Clone git repositories with exec_mac. - create_mac_upload_link: a link that puts one file of up to 2 GiB on the Mac. Give its url to the human (they choose the file in a browser, no account needed), or upload with its curl command. get_mac_file_link shows when the file is on the Mac. - create_mac_download_link: a link to a file or folder on the Mac (folders come as a zip), up to 2 GiB, for the human or for curl. It keeps working after the Mac stops, until it expires (one hour by default, a day at most). - open_mac_desktop: a link to the Mac's full desktop in the human's browser, with mouse and keyboard, for when they want to see or click something (Xcode, the Simulator, a sign-in window). The session must be ready; the human signs in to nomac.app. The page disconnects after 15 minutes without input. macOS has no RDP. To show the iOS Simulator there, boot a device with simctl, then run: open -a Simulator || open -a DeviceHub (Xcode 27 replaced Simulator.app with Device Hub; Xcode 26's Simulator.app, kept beside it, shows iOS 27 devices too) - stop_mac: request shutdown after retrieving work. Poll get_mac_session until cleanup_confirmed. A stopping state is not a deletion receipt. ## A Mac for agents Every Mac is fully enabled for the agent: nothing on it waits for a person. The account is agent, an administrator; sudo needs no password. Each session has its own login password, in $NOMAC_MAC_PASSWORD on the Mac (and from nomac password or GET /api/v1/mac/sessions/{id}/password): use it for the login keychain (security unlock-keychain -p "$NOMAC_MAC_PASSWORD", then security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$NOMAC_MAC_PASSWORD" after importing a certificate) and any password prompt. macOS asks for no administrator password, and the dialogs it still shows are answered on the Mac within seconds: privacy prompts ("App would like to access the Camera") with Allow, and password dialogs (the login keychain, the few rights macOS keeps even from root) with the session password and Always Allow. macOS's request to turn on Accessibility for a program is closed: it only opens System Settings, and the programs commands run under already have Accessibility. Each is listed in ~/.nomac/allowed.log. The screen never sleeps or locks by itself. nomac-permit APP camera microphone (or all) on the Mac grants an app privacy permissions by bundle ID before it asks, also one built later (CLI: nomac permit; at start: access.permissions or nomac start --permit APP=camera,microphone). Accessibility and screen recording are turned on in System Settings, Privacy & Security, where no password is asked. ~/NOMAC.md on the Mac says the same. Everything is deleted when the session stops, and nothing outside the session can reach the Mac. To keep macOS's usual prompts, start with access prompts "ask" (CLI: nomac start --prompts ask). The account can save options every new Mac starts with (GET, PUT or DELETE /api/v1/mac/defaults; CLI: nomac defaults; the dashboard), and a start's own access wins for that Mac. get_mac_session's account shows the Mac's state, Gatekeeper and what is answered for the agent (prompts_answered) included. People on a Mac can open it in Apple's Screen Sharing with nomac desktop --app. ## What's on the Mac macOS 26.6.2, Xcode 27.0 (default) and Xcode 26.6 (/Applications/Xcode-26.6.app), iOS 26.5 Simulator, iOS 27.0 Simulator. To build with Xcode 26.6, set DEVELOPER_DIR=/Applications/Xcode-26.6.app/Contents/Developer for a command, or run sudo xcode-select -s /Applications/Xcode-26.6.app (no password asked). Installed and on every PATH (commands, login shells and SSH): Node 24.20.0, npm 11.19.0, Ruby 3.4.11, Bundler 4.0.22, CocoaPods 1.17.0, xcbeautify 3.2.1, XcodeGen 2.46.0, Git 2.54.0, Git LFS 3.8.0, GitHub CLI 2.100.0, jq 1.7.1, Python 3.9.6, Swift 6.4, Homebrew 7.0.8. Homebrew belongs to the agent account, so brew install works. Commands may take screenshots (screencapture), drive any app with AppleScript and System Events, and read every folder without a permission prompt. The screen never locks and shows no permission dialogs. Pipe xcodebuild through xcbeautify to keep output short. get_mac_session returns the same list as image. ## Time, recovery and current limits Credit is $0.80 an hour, charged on each session's ready time rounded up to whole minutes, with a one-minute minimum: a five-minute build costs about 7¢, an hour 80¢, a full eight-hour day $6.40. One session at a time; no automatic overage. Ready time includes time between commands; queue/setup time are free. Sessions stop when credit, their spend cap ($5 by default, enough for 6 hours 15 minutes) or their duration limit runs out. Defaults are a two-hour session (eight at most) and a ten-minute idle timeout. Credit never expires. SSH disconnect is not shutdown. The idle timeout counts from the last command, SSH connection or desktop viewer (the nomac CLI keeps its SSH connection open for 2 minutes after the last command); processes left running after SSH closes do not count, so raise idle_timeout_seconds (up to 3600) for unattended background work, and always stop. After an unknown response, reuse the same request_key AND unchanged arguments. Never rerun an unknown command with a new key. Inspect the saved session/job. Output retrieval failure does not mean command failure. Managed commands default to 300 seconds and cap at 600; raise timeout_seconds for slow work, such as a first iOS Simulator boot, which needs about 90 seconds of CoreSimulator data migration. SSH remains available for longer work within the funded session. Output retention does not terminate a verbose command: the executor keeps the first 8 MiB combined and a final 64 KiB per stream when truncated. At EOF, tail_base64 and absolute tail_offset identify the retained tail; deduplicate overlap with the prefix. Older completed logs can return output_expired (410). The command receipt still exists: do not rerun it to retrieve logs. Guest output and files disappear at stop; external retention is not implemented. CLI sync (0.9.0 and later) sends every file, .env files and keys included, and symlinks as links, never following them. It skips node_modules, Pods, DerivedData, .build, build and .git, and the folders .gitignore lists. In .nomacignore, a ! line sends any of them and other lines skip more; each sync lists what it skipped. Older CLIs also skip .env files, keys and symlinks. Sync never deletes remote files: remove obsolete remote source files explicitly after a rename. write_mac_file, read_mac_file, fetch_to_mac and file links move single files, not whole workspaces. See the screen with screenshot_mac, then type and click with mac_input, or drive apps with AppleScript and System Events (any app, no permission prompt); drive the Simulator with simctl, and hand the human open_mac_desktop when they want to see it. Park/save and persistent disks are not currently offered. A command can be at most 64 KiB in total (environment values at most 8 KiB each); retrying a larger one fails again. Put files on the Mac with write_mac_file (up to 3 MiB each), fetch_to_mac (any size, from a URL), an upload link (up to 2 GiB, from anywhere), git clone, or nomac sync for a whole folder. For Mac support, call report_issue with description, session_id and optional job_id. Only that account's session state is attached. Omit description to read replies. Never include passwords or signing secrets in a report. ## Secrets, signing and shipping Secrets: each account has one store of variables and files. The human enters them at https://nomac.app/secrets, from their terminal (nomac secrets set NAME, or --from-env-file .env), or through a link from request_secret. Never ask for a secret in the chat. Values are encrypted and no API returns them. Every new Mac receives them: variables in every command's environment (exec_mac, SSH, login shells), files at their path (default ~/.nomac/secrets/NAME). Their values are masked in command output. list_secrets shows names; install_secrets (nomac secrets install) puts a new or changed secret on a running Mac. A secret set to on_request is installed only when named. Apple: the human connects an App Store Connect team key once at https://nomac.app/apple. The key stays on NoMac's server; it never reaches a Mac or the chat. Signing: every Mac starts with the account's distribution certificate in a keychain and the NoMac App Store profiles made so far. set_up_signing (CLI: nomac sign [path]) reads the project on the Mac (the app and every extension, with their entitlements), registers missing bundle IDs, switches on capabilities, makes and installs the App Store profiles, and returns a build settings file (.xcconfig), export options, the next build number (reserved for the app) and the exact xcodebuild, Flutter and fastlane commands. The project itself is not changed: build with your own tools. A certificate is created at Apple only when the account has none. The human can upload their own .p12 at https://nomac.app/apple instead (from Keychain Access, OpenSSL, Expo or fastlane); a Developer ID certificate signs Mac apps. Steps only a person can take (an App Group or iCloud container, agreements, Apple's certificate limit) come back as needs_human with a link. Expo and Capacitor apps need their ios folder first (npx expo prebuild -p ios, npx cap sync ios). TestFlight: send_to_testflight (CLI: nomac testflight path/to/App.ipa) sends a signed .ipa from the Mac. NoMac creates the upload at Apple, the Mac sends only the file, Apple processes it (usually 5 to 15 minutes, no Mac needed), and NoMac puts it on TestFlight for the internal testers with your notes as What to Test. Repeat the call with its request_key to follow it. Apple asks once per app whether it uses encryption beyond HTTPS and Apple's own: relay the human's answer as uses_non_exempt_encryption, never guess. The app must exist in App Store Connect (a person creates it once). A build number Apple already has comes back with the next free one. App Store: once the send is on_testflight, publish with its project_id and build_id stages the version and returns Apple's blockers without submitting; set What's New with set_metadata; publish with confirm:true submits for review after the human approves. CLI: nomac publish App.ipa --whats-new "...", then the printed command with --confirm. Every App Store tool below takes that project_id. Mac apps: sign every binary with the Developer ID Application certificate, the hardened runtime and a secure timestamp (codesign --force --options runtime --timestamp --sign "Developer ID Application: ..."), then notarize (CLI: nomac notarize App.app) sends the .app, .dmg, .pkg or .zip to Apple's notary service and staples the ticket. Keep the Mac running until it is stapled. Your own tools (fastlane, altool, Transporter) need the .p8 itself. The human may store it as a secret file; that is their choice, and the key is then on every Mac. To import a .p12 by hand instead, for example one stored as a secret file: Apple's WWDR intermediates (G3-G6) are preinstalled, so no trust change is needed. Create and unlock a keychain, then: security import cert.p12 -k "$KC" -P "$P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASSWORD" "$KC" security list-keychains -d user -s "$KC" ~/Library/Keychains/login.keychain-db security find-identity -v -p codesigning # expect 1 valid identity A .p12 exported by OpenSSL 3 with default settings fails to import ("MAC verification failed"); re-export it with openssl pkcs12 -export -legacy. ## Related For Capacitor apps, the native iOS shell needs a Mac only when native code changes. Web changes can ship over the air with OtaKit (https://www.otakit.app), an open-source live update service for Capacitor from friends of NoMac. ## Existing publishing product (closed to new subscriptions) The Builder's build tools (push_project, check_project, build, status, get_failure) serve existing Builder subscribers only. The Builder plans are closed to new subscriptions and no checkout exists for them. They are not needed for, and do not include, cloud Mac sessions: for a Mac, use prepaid Mac credit (above). The App Store and TestFlight tools further below work for every account with Apple connected, on an app made by send_to_testflight or a Builder project. ## For existing Builder subscribers Run `push_project` then `check_project` for source-readiness findings. Fix source findings; compilation, signing and TestFlight remain unverified. Use `get_billing_status` for activation, the renewal amount, the quota reset in UTC and the guarantee deadline. Finish Apple setup in /wizard: upload the .p8 file (Key ID is inferred from its name), enter Issuer ID, and verify. A build without an active plan returns build_started:false. After setup, resume once with a fresh request_key. If a request's outcome is unknown, reconcile it using its existing request_key before starting any new build. A subscriber can switch to Mac credit at https://nomac.app/usage: the subscription ends and its last payment becomes Mac credit. Only the account owner can do this; an agent cannot. Refunds are human-only: agents cannot request one. If the human asks, send them to https://nomac.app/usage (or support@nomac.app). Once they have requested it, `get_refund_status` tracks the refund and cancellation. ## Get help from NoMac support Use `report_issue` as soon as you hit an unknown or infrastructure error you cannot fix. Pass `description` and the failing `ref_id`; NoMac attaches the relevant build logs and lint context automatically and deduplicates repeat reports. Call the same tool with no description to read ticket replies. The optional `email` field requests a direct update when the ticket is resolved. It is never required because replies remain available through `report_issue`. If the tool returns a Private Relay warning, explain that Apple relay addresses may reject later support messages and offer to provide a non-relay follow-up address. Do not show that warning for normal addresses, including `@icloud.com`. ## Ship an app to TestFlight with the Builder 1. `push_project`: pack + upload the working tree (secrets never leave the machine); auto-detects the Xcode project; runs source-level review lint. 2. `build` with workflow=release: signed build on a cloud Mac → uploaded to TestFlight. Poll `status` (~3 minutes total). workflow=smoke is a fast unsigned compile check. 3. When state=ready, tell your human to open TestFlight on their phone; the build is there. They tap around; you read crashes/comments with `get_feedback`. Fix, push, build again. 4. If a build fails: `get_failure` returns {stage, code, kind, error_lines, hints}. kind=project → you fix the source. kind=account → relay the deep link to your human. ## Pass App Store review (the part everyone else skips) 1. `review_lint`: deterministic readiness report. Fix reds: some findings include ready-to-apply patches (e.g. a generated PrivacyInfo.xcprivacy); metadata reds are fixed via `set_metadata`. 2. `get_metadata_schema` then `set_metadata`: YOU write the listing copy (description, keywords, name…); NoMac validates limits and writes to App Store Connect. Also: primary_category, age_rating (ask your human: these are attestations), content_rights, copyright, review_contact, price. 3. `upload_screenshots`: exact-dimension validated replacement with backups. Poll `get_screenshot_upload` until complete; follow recovery instructions if a replacement is paused or rolled back. 4. `publish` without confirm → staging (changes drafts, but does not submit); Apple's own blocker list comes back structured if anything is missing. `publish` with confirm:true → submitted for review (requires your human's authorization). New versions use manual release; existing versions retain Apple's release policy. 5. `get_review` lists live submissions, including ones created outside nomac. Pass asc_submission_id to inspect every item and its version/build. Apple does not expose reviewer messages through its public API: open the provided App Store Connect link and have your human copy the message to you. `prepare_review_reply` prepares your draft and always returns sent:false; the human sends it in App Store Connect. Updating review notes is not a reply. 6. Fix metadata/screenshots on the exact version, or send a fixed build (send_to_testflight, or a Builder build). `publish` accepts exact build_id and asc_submission_id; resolve_rejection:true explicitly marks that version's rejection fixed. `update_review` supports resolve_item/remove_item/cancel/resubmit/release. Preview without confirm; confirm:true performs the selected action. Resolve/remove ALL rejected items before resubmitting. A removed item cannot be added back to that submission. Poll get_review; release requires Pending Developer Release. ## External TestFlight 1. `get_testflight` reads groups/test info and recent builds; pass asc_build_id for the exact beta-review and external distribution state. group_id lists testers. 2. `manage_testflight` action=metadata sets app_info (description, feedbackEmail, URLs), review_contact (Apple field names, demo access), locale and whats_new. 3. Select/create an external group; invite_tester/remove_tester manage explicit testers. update_group can enable a public link with a tester limit. 4. action=distribute assigns the exact asc_build_id to explicit group_ids and submits Beta App Review if required. auto_notify defaults false; action=notify starts testing after approval. remove_build removes access for one group; action=expire stops testing for all groups. All actions preview without confirm and apply with confirm:true. 5. Poll the exact build. Internal ready is NOT external approval. Beta rejection messages require App Store Connect; fix the issue and upload a new binary when needed. action=encryption relays a human-provided uses_non_exempt_encryption answer; never guess a compliance answer. ## Manage the rest of the App Store lifecycle 1. `get_store_capabilities` lists available operations by category/search and next_offset. Pass operation for the exact Apple JSON:API request/query schema. Categories cover listing, releases, pricing, purchases, subscriptions, reviews, marketing, assets, testflight, analytics, compliance and webhooks. 2. `read_store` calls a listed GET operation. scope:[] is the project's linked app. Start with an apps_* relationship and copy each returned resource's scope into subsequent calls. Follow next_cursor with the same operation/query. Global territories/categories use resource_id; app resources require scopes. 3. `update_store` previews a listed write with its schema-valid body. Supply scope for the exact target or creation parent; existing relationship IDs need their scopes in references. Inline creates use local IDs like ${price1}. confirm:true performs the real write using authorization already provided. 4. Paid prices and availability, IAP/subscription products, localizations, offers, public customerReviewResponses, phased release, events/custom pages/experiments, analytics reports, beta diagnostics and app webhooks use this catalog. First IAP approval may need the app version submission. Use publish, update_review and manage_testflight for submission/release/distribution. 5. `upload_store_asset` reserves and uploads new previews, screenshots, product images, supplemental reviewer files and encryption documents. Pass the create operation and parent JSON:API body. CLI accepts file_path (up to 500 MiB); hosted MCP accepts data_base64 (up to 2 MiB). Apple asset limits also apply. Preserve request_key and file bytes to resume. uploaded:true only means the transfer committed; poll read_store until assetDeliveryState is COMPLETE. Existing files are not replaced; use upload_screenshots for managed replacement. Public customer review replies are posted to the App Store. They are separate from messages sent by Apple reviewers; prepare_review_reply remains an unsent draft. Supplemental review attachments are supported; correspondence/appeals and the App Privacy questionnaire still require App Store Connect. Supply owner-approved pricing, public text and compliance answers. App-scoped webhook subscriptions can notify your own receiver; nomac does not start an agent loop. Analytics segments expose Apple's download URLs; offer-code CSV exports return base64 with a 2 MiB inline limit. Account-wide finance/admin and simulator APIs are outside these tools. get_store_capabilities returns the full limits. For any pending store/review/TestFlight mutation, repeat the unchanged arguments with its request_key. `get_store_operation` recovers lost responses and pending actions. Use a fresh key for a new action or changed inputs. ## Rules of the road - Builder builds cost quota (Starter 50 a month, Pro 300); smoke builds also count, so don't loop release builds to debug compile errors. Builds sent from a Mac with send_to_testflight cost only Mac time. - Never guess at store answers that are legal attestations (age rating, data usage/privacy label): ask your human, then relay. - Do not retry-loop an identical unknown failure; file it with `report_issue` and poll the same tool for a reply. ## Endpoints & docs - REST: https://nomac.app/api/v1/* (Authorization: Bearer nmk_…) - Hosted MCP: https://mcp.nomac.app/mcp - Install page for agents: https://nomac.app/install - API reference (every endpoint, shapes, errors): https://nomac.app/docs/api - CLI reference: https://nomac.app/docs/cli - Builder by hand and in CI (existing subscribers): https://nomac.app/docs/cli#ci