Help · Agents
Reference
For readers building their own integration or reading what the agent did. The shipped guide
the agent reads is at AtControl for Mac.app/Contents/Resources/atcontrol-agent/ — SETUP.md,
SKILL.md, SKILL.manual.md, scripts/atcontrol.py, scripts/atcontrol.sh, mcp/atcontrol_mcp.py.
Two ways in
| App Intents / Shortcuts | URL scheme (deeplinks) | |
|---|---|---|
| Direction | request and response (JSON) | one-way; open proves only that macOS accepted the URL |
| Identity of the caller’s session | the returned session id, or the caller’s own request_id |
only a request_id the caller chose itself |
| Protection | none needed — only a program already running as you can call an intent | Refuse links opened by apps (default): refuses links opened by an app with a window, open from a command line passes · Require a token: token= in the query, visible to anything that can list your processes · No protection |
| Switch | Settings ▸ Agents ▸ Access ▸ Shortcuts and App Intents (on by default) | Deeplinks (off by default) |
| Refusal when off | agent_surface_disabled |
silent; recorded in the journal |
| Refusal by the protection | — | deeplink_sender_refused (opened by an app) or authorization_required (missing or wrong token), silent; recorded in the journal |
Both share one session registry. Switching a surface off refuses new commands on it before anything changes; sessions already running keep their leases and remain endable through the other surface.
The five shortcuts
AtControl Begin Session (task, optional request id, optional after work: none,
sleep or shutdown) → session id and the timeout settings · AtControl End Session · AtControl End Session and Sleep · AtControl End Session
and Shutdown (each: session id or request id) · AtControl Agent Status →
the registry, both surface switches, guide_path, and the wrapper version it expects and
last saw.
An ending command names its session by session id (the UUID the app returned) or by
request id (the caller’s own key); both in one command is refused as
conflicting_identities; neither is valid only while exactly one session is active
(otherwise ambiguous_session). Sleep after the end is deferred while other agents run and
happens when the last one finishes.
After work is what the agent says should happen once it is done. Only a session that says
sleep or shutdown is subject to the session timeout: if it is still open when the time is
up, AtControl ends it and puts the Mac to sleep (never shuts it down, and not while you hold the
Mac awake yourself). A session that says none, or nothing, has no time limit. A second Begin
with the same request id and task may change what a running session says — that is how a hook
starts a turn and the agent adds a sleep request later. Every session in a response carries
after_work, and only one that can expire carries expires_at.
Armed after work is what you set on a running session from AtControl — Sleep instead on
the line that says what happens after the agents (the band under the status card in the menu bar, or the Agents
card of the window), or Sleep when the agents finish on the phone; it arms
every session running at that moment.
The session reports armed_after_work: "sleep", comes under the session timeout counted from the
moment it was armed, and ends with a sleep request however the agent ends it. The agent cannot
withdraw it; you take it back with Stay awake.
Every shortcut also stamps a wrapper version. Status reports wrapper_version_expected,
wrapper_version_seen and wrapper_outdated; Settings ▸ Agents ▸ Setup says it in
words, on the Shortcuts row. An old wrapper is never refused, but a parameter it does not know is silently
dropped by Shortcuts — reinstall when told. The current generation (3) added after work to
Begin; generation 2 dropped the token parameter. Shortcuts installed before it report as
outdated, and an old Begin silently drops after work, so a sleep request gets no timeout:
delete them in Shortcuts and install again from Settings ▸ Agents ▸ Add to Shortcuts….
The URL scheme
Off by default: switch on Settings ▸ Agents ▸ Access ▸ Deeplinks first. Query names are case-sensitive;
duplicate or unknown parameters are rejected; percent-encode task text. Under Require the
capability token, every URL carries &token=… (the examples below leave it out).
atcontrol://begin-session?task=Build%20the%20beta&request_id=<UUID>[&after_work=sleep][&token=…]
atcontrol://end-session?id=<session UUID>
atcontrol://end-session?request_id=<UUID>
atcontrol://end-session-and-sleep?id=… | ?request_id=…
atcontrol://end-session-and-shutdown?id=… | ?request_id=…
atcontrol://sleep # waits for all agents, like End and Sleep
atcontrol://block-screensaver
atcontrol://unblock-screensaver # cancels a timer; leaves agent sessions running
atcontrol://ping # reaches the app and does nothing — no journal row, no window
Use open -g so the app is not brought forward. The whole setup for a deeplink-only
integration:
ID=$(uuidgen)
open -g "atcontrol://begin-session?task=Nightly%20build&request_id=$ID"
# … work …
open -g "atcontrol://end-session?request_id=$ID"
The helper CLI (what the agent runs)
python3 <guide>/scripts/atcontrol.py or <guide>/scripts/atcontrol.sh — one contract, one
JSON object per call. Public API from the first release: commands, flags, output keys and
the state-file fields are only ever added, never renamed or removed.
| Command | Does |
|---|---|
doctor |
Read-only status; reports missing_wrappers, agent_surface_disabled, shortcuts_unavailable, deeplinks_enabled, app_intents_enabled, guide_path |
check |
The setup proof: begins and ends “AtControl setup check”, no power |
permissions |
Runs each of the five shortcuts once so macOS asks about each while you are at the Mac; reports shortcuts (per name: ran, not_run or the error) and power_requested. No power: two guard sessions “AtControl permissions check” stay open while Sleep and Shutdown are sent for a request nobody began, which AtControl refuses |
begin --state FILE --task TEXT --action sleep|shutdown|none |
Saves a request UUID before calling, then the session UUID; a retry with the same state reuses it (reused: true) |
finish --state FILE [--release-only] |
Ends the saved session with the saved action; a repeat returns the saved result (repeated: true); after two unanswered attempts stops with finish_uncertain |
hook [--state-dir DIR] [--label TEXT] |
What a host’s hook runs. Reads the host’s event as JSON on stdin: UserPromptSubmit begins a turn, Stop ends it (with the recorded action), StopFailure, Interrupt and SessionEnd release it, the last two in the background. Prints nothing, or {"systemMessage": …} on failure; always exits 0 |
after-turn --action sleep|shutdown|none [--host-session ID] |
Records, during a hooked turn, what happens when its reply is complete. Names the turn by the host’s session id (Codex: $CODEX_THREAD_ID) or by the only open turn; no_hook_turn / ambiguous_hook_turn otherwise |
task --name TEXT [--host-session ID] |
Names the current hooked turn with what the agent is doing (3–6 words); AtControl shows it as Host: folder: name. Requests no power; no_hook_turn when there is no turn to name |
Exit codes: 0 done · 1 AtControl refused or failed (see error) · 2 usage · 3 a
prerequisite is missing, nothing sent · 4 the state file disagrees or ownership is uncertain,
nothing sent.
sleep_state values in a finish: scheduled / requested (not proof of physical sleep),
deferred (other agents still hold the Mac), simulated (Service Mode suppressed the
physical action, developer builds only).
The helper goes through Shortcuts and needs no token; it reads and writes no token file.
The MCP server
/usr/bin/python3 <guide>/mcp/atcontrol_mcp.py, a stdio server for hosts whose sandbox blocks
Shortcuts (Codex) or that have no shell. Tools: atcontrol_status, atcontrol_check,
atcontrol_permissions, atcontrol_begin (task, after_work), atcontrol_finish (session, release_only),
atcontrol_after_turn (after_work, host_session), atcontrol_task (title, host_session). A session can be finished only by the
connection that began it; every sleep or shutdown asks you first through the host (MCP
elicitation), and a host that cannot ask is refused; closing the connection releases every
session it still holds, never with power.
Rules the agent follows (from the shipped SKILL.md)
- Register before work; finish as the very last action, after saving artifacts and a completion note — the Mac may sleep before another message can be sent.
- Never end another agent’s session, never guess “the only session”, never create a replacement session to send a second power request.
- A hook never decides on sleep or shutdown: hooks fire per reply, not per task. It keeps the
Mac awake for each turn and, at the end of the reply, acts only on a sleep the agent recorded
in that same turn (
after-turn), on your explicit request. - With a hook, don’t register or finish for the turn: record sleep or shutdown with
after-turnas the last action of the turn that completes the work, and never in a turn that ends with a question to you. - Report the real
sleep_state; never bypass Service Mode or any app setting. - On a budget or interruption warning: stop starting work, save a checkpoint,
finish(release-only unless the user authorised power on that kind of stop).
Settings that shape sessions
- Session time limit — default 8 hours, counted from each agent’s start. It applies only to agents asked to sleep or shut down the Mac when their work is done, and to a session you armed yourself (counted from the arming): if one is still running when its time is up, AtControl puts the Mac to sleep (never shuts it down), and not while you keep the Mac awake yourself. Every other agent can work for as long as it needs. Off removes the limit altogether.
- Bring AtControl to the front — Never, For sleep and shutdown (default: ordinary commands stay behind, a sleep or shutdown request comes forward), or For every command.
- Low battery (Settings ▸ Keep Awake) releases agent sessions too.
Security notes
A session id and a request id are identifiers, not secrets: any local process that can open a
URL or run a shortcut can end a session whose key it knows, exactly as it could with the id.
The URL scheme’s protection guards against callers that can open a URL but cannot run a
command — a web page, a link in mail or a message, a QR code. Refuse links opened by apps
(recommended, the default) reads the sender from the macOS open-URL event and refuses any app
with a user interface; command-line tools pass, and if the sender cannot be told the link is
refused. A program already running as you can still run open — the same exposure Shortcuts
has — and a sandboxed app that runs open probably passes too (not verified). Require the
capability token also stops command-line tools, at the cost of a secret that is visible while
open runs and must be given to every agent that uses links. No protection lets a web page
or a link sleep, shut down or keep the Mac awake. Anything that can run a shortcut is already
running as you and could read a token file, so App Intents take none. Task names are
normalised before they are stored: control characters and line breaks become spaces,
invisible direction and zero-width marks are dropped. AtControl never executes the helper scripts itself and downloads no code.