Agents

Delegate to AI

MCP lets an assistant come to Cotask. Delegate to AI is the other direction: you push one task out to an agent you run. Each place a task can go is a route, and there are two kinds:

  • Claude Code or Codex on your Mac — nothing to host. Cotask keeps the endpoint; the Cotask Mac app's Delegate Listener picks the task up and runs it through the CLI. See Run it on your Mac.
  • A webhook URL — an n8n flow, a Lambda, a bridge of your own, anything that accepts a POST.

You set routes up; delegating through them is yours, and — when you ask it to — an assistant's you connected: delegate_task over MCP (or an agent key on POST /agent/delegate) is the same hand-over as the button. An assistant uses a route exactly as you set it up: it can add instructions and custom parameters, never change the route's model, effort, folder or access. So a route with full access to a folder is one any assistant you connected can start — keep full access for routes you'd hand to them. Delegate to AI is part of Pro.

Configure a webhook#

Where: iPhone → Settings → Delegate to AI → New route → Webhook URL. Mac → Settings → Agents → New Route → Webhook URL….

  1. Name it after the agent behind it — Claude, ChatGPT, n8n… The name picks the logo, exactly like an agent key, so a list of routes reads as a list of agents. Left blank on the Mac, it takes the URL's host.
  2. Paste the URL Cotask should POST to. It has to be public — localhost, private addresses, .local and .internal are refused. Each route shows where it goes and when it was last used.
  3. Optionally add parameters and a signing secret — tap the route on the iPhone, or its slider button on the Mac, to edit it.

Run it on your Mac#

A Mac route hands the task to Claude Code or Codex on your own Mac, from the command line, with no server of yours in between.

Where: iPhone → Settings → Delegate to AI → New route → Claude Code or Codex on my Mac. Mac → Settings → Agents → New Route → Claude Code or Codex on This Mac…. Both apps edit the same routes with the same editor — CLI, model, effort, access, folder, parameters and the inbound token; the Mac lists each as "Claude Code · opus · high on your Mac", with a slider button to edit it.

  1. Pick the CLI — Claude Code or Codex.
  2. Pick the model and effort. The lists are not written into the app: they are what the CLI on your Mac says it offers, read when its listener last checked (Claude Code through its SDK handshake, Codex through its app server), so a new model shows up the day your CLI has it. Each model offers only the efforts it takes. When your Mac hasn't reported yet, or a CLI can't list its models, type a model id instead — it is checked here, again on the Mac, and the CLI has the last word. Leave both blank for the CLI's own defaults.
  3. Access — how much it may do:
  4. Read only — reads and answers, changes nothing. Claude Code gets only its Read, Glob and Grep tools, ignores settings files (and the permissions and hooks in them) and starts no MCP server (--restricted --tools Read,Glob,Grep --strict-mcp-config --permission-mode dontAsk). Codex runs in its read-only sandbox without your ~/.codex/config.toml, so none of its MCP servers start either (--sandbox read-only --ignore-user-config). Read only stops changes; it doesn't hide files — Codex's sandbox can still read outside the folder. With no MCP server it also can't mark the task done — its answer is in the run's log.
  5. Edit the folder (default) — edits files in its folder; anything that would need approval is refused (acceptEdits · workspace-write).
  6. Full access — runs anything without asking (--dangerously-skip-permissions · danger-full-access). Only for folders you would hand to it unattended.
  7. Folder — where it runs, like ~/Projects/app (Choose… on the Mac). Blank uses the Mac's Default folder (Settings → Delegate Listener; your home folder unless you choose another). It must exist on the Mac and sit inside your home folder once links are followed; absolute or ~ paths only, no ...

Project folders (optional) — iPhone → Settings → Delegate to AI → Project folders; Mac → Settings → Agents → Project folders. Give a Cotask project a folder, and its tasks run there whichever Claude Code or Codex route they're sent to. The order, most specific first: a folder typed for one send → the task's project folder → the route's folder → the Mac's default folder. Leave a project blank to keep using the route's.

Through the API, a Mac route's folder and access are two of its parameters — cwd, and access as read_only, workspace_write or full_access.

The iPhone shows your Mac above the routes: whether it is listening, and each CLI's version, whether it is signed in, and how many models it offers.

Delegating to a Mac route queues the task. The toast says Sent to Claude Code when a Mac is listening — its listener checked in within the last three minutes — and Queued for Claude Code — it runs when your Mac is listening when none is. Work nobody picks up within a day expires rather than starting late and by surprise. Only you, signed in to the app, can create a Mac route or change how it runs; an agent key can rename one, not give it more access.

The Delegate Listener (Mac)#

Where: Mac → Settings → Delegate Listener → Listen for delegated tasks. Off by default.

Switched on, Cotask sits in the menu bar and listens — Realtime for the moment a task arrives, and a check every 20 seconds so nothing is missed across sleep or a dropped connection. It checks in every minute, which is what the iPhone reads as listening. For each task it:

  1. claims it — one conditional update, queued → running, that only one Mac can win, so a task never runs twice, even with Cotask open on two Macs;
  2. posts a notification — "Sent to Claude Code", with the route, the model and the task's title (never its parameters);
  3. runs the CLI — started directly with an argument list (no shell), the prompt piped to its standard input, in the route's folder, with the route's model, effort and access:
  4. claude -p --input-format stream-json --output-format stream-json --verbose --replay-user-messages <access> [--model …] [--effort …]
  5. codex exec --json --skip-git-repo-check --color never --cd <folder> <access> [--model …] [-c model_reasoning_effort="…"] -

Both stream their events as JSON lines, so the run's log is its transcript — what Get summary reads. Claude Code's input stays open while it works, so a follow-up can be written into it.

  1. records how it went — done, or failed with the end of what it printed (anything shaped like a key or token is blanked out), and a notification either way: "Claude Code finished", "Claude Code failed" with the reason, or "Couldn't start Claude Code" when the model, effort or folder is refused before anything runs.

That record is the run's, not the task's: the listener never completes a task or writes on it. The agent does that itself, through the Cotask MCP server, when it has one — which is what the prompt asks of it. A run that fails leaves the task in Doing, and says so on the Mac.

Up to five runs at once by default, Claude Code and Codex together, oldest first — Runs at once in the pane changes it (1 to 20). When that many are going, the rest wait in the queue until one finishes. A run is stopped after two hours ("Stopped after 120 minutes."). Switched off, it takes nothing new — no claim, no run — and runs already going finish unless you stop them. Quitting Cotask, or signing out, stops them as well, and they are recorded as canceled.

The menu-bar item shows what it is doing, each run in progress with Stop This Run (and Stop All Runs when there are several), each CLI's health, the last five runs, Check CLIs Again and Stop Listening. Removing the icon from the menu bar switches the listener off.

The pane also shows the health of each CLI, checked natively: installed, version, signed in (claude auth status --json, codex login status), and the models it offers. Check again re-reads them, and a path can be set by hand when a CLI lives somewhere unusual — otherwise Cotask looks in your login shell's PATH and the usual install folders (Homebrew, npm, bun, Volta, ~/.local/bin, ~/.claude/local). Running now lists the runs in progress, each with Stop; Recent runs lists the last eight with their errors, and a button to show each run's log in the Finder. Each CLI also has a Summary model — what Get summary asks for (below); blank uses sonnet for Claude Code and gpt-6-luna for Codex.

Each run's full output is logged under ~/Library/Logs/Cotask/Delegate Listener/<dispatch id>.log, readable by you only. The log is the raw output — only the error and the notifications are scrubbed.

Follow-ups on the Mac#

A follow-up goes back into the conversation the task already has, instead of starting Claude Code or Codex over with the whole task:

  • While the run is still going, it is steered in. Claude Code gets it on its input and reads it mid-task — at its next step, without stopping. Codex can't take anything once it is going, so the step it is on is interrupted and the same thread resumed at once with the follow-up; it keeps everything it had done and said. Either way the follow-up needs no free slot, the run's two hours start again, and both runs end together, with the same summary. Cancel run on either stops the conversation.
  • Once the run has ended — done, failed or stopped — the follow-up resumes its session (claude --resume <session>, codex exec resume <thread>), with the route's access as before.
  • Only when the session isn't on this Mac — the run was on another Mac, the session was deleted, or the route now uses the other CLI — does it start a new session with the whole prompt.

The Mac reports each session's id as the CLI names it, and the follow-up's working line in the timeline says which it was: Sent into the run that was already going, Continued the earlier session, or Started a new session. A follow-up whose run is going on another Mac is left for that Mac. Steering and resuming share the run's log, so Get summary on a follow-up reads the whole conversation.

The CLI inherits the app's environment, plus PARALLEL_DISPATCH_ID, PARALLEL_ROUTE_ID, PARALLEL_TASK_ID (when there is a task), and every route parameter as PARALLEL_PARAM_<NAME> — PARALLEL_PARAM_CWD and PARALLEL_PARAM_ACCESS included.

More than one Mac#

With one Mac nothing here appears. Once two or more Macs have listened in the last 30 days:

  • A Mac route's editor gets Runs on: Any Mac (the default — the first Mac listening takes the run) or one Mac by name. Only that Mac can take the route's runs; moving the route moves its queued runs with it, and a waiting run says which Mac it is waiting for.
  • Project folders get Folders on: a folder for every Mac, and a folder for one Mac that wins on that Mac.
  • Settings shows each Mac's status, and a route's editor shows its Mac's CLIs and models.

Over the API, a route's listener_id names its Mac, and list_delegate_routes reports each route's device.

Usage limits#

Where: iPhone → Settings → Delegate to AI → Usage limits. Mac → Settings → Usage Limits. Both appear once a Mac listens for Mac routes; with none, there is nothing to show and neither does.

How much of your Claude Code and Codex plans is used, Mac by Mac — each Mac is signed in to its own CLIs, so with two Macs on two accounts you see both. For each CLI: the plan (Max, Pro Lite) and a bar per limit — the current 5-hour session, the week, and the week for a single model when the plan has one — with how much is used and when it resets. Claude Code's bars are in Claude's orange, Codex's in white; any bar turns warning orange at 80% and red at 95%. The Usage limits row shows the tightest limit of each CLI across your Macs: Claude 64% · Codex 38%.

The Mac asks each CLI itself, and it costs nothing — no prompt is sent: Claude Code's get_usage request over --input-format stream-json, and Codex's account/rateLimits/read over codex app-server. It asks when the listener starts, every five minutes, and after every run, and publishes only the plan, the percentages and the reset times — never the account behind them. Each Mac's card says when it was last updated, or Offline and when it was last heard from; a window whose reset time has passed reads 0% until the Mac checks again. A CLI signed in with an API key has no plan limits, and says so. On the Mac, Check This Mac Now asks again at once.

What the CLI is told#

The prompt is written by Cotask's server, once, so every Mac gets the same words:

You have been handed a task from Cotask (the user's task manager) through the "Claude Code" route.

Task: Draft the release notes
Task id: …
Status: doing
Project: Cotask
Due: 2026-09-01
Priority: high

Notes:
…

Attachments (public URLs you can fetch):
- Spec — Notion: https://…

Instructions from the user:
Match the tone of the last one.

Parameters:
- branch: main
- access: workspace_write

When you are done, reply with a short summary of what you did and where the result is. If the
Cotask MCP server is available to you, mark the task done with that summary as its completion note.

Lines with nothing to say are left out. Work queued with a prompt and no task gets the request in place of the task. The whole prompt is capped at 60,000 characters.

A follow-up that goes back into the session is told only what is new — the agent already has the rest:

A follow-up from the user on the task they handed you from Cotask through the "Claude Code" route.

Also update the changelog.

The task as it stands now:
Task: Draft the release notes
…

With nothing typed, it says the task was sent again and asks the agent to carry on where it stands. A follow-up that has to start a new session gets the whole prompt above, its instructions included.

To have the agent close the loop, give the CLI the Cotask MCP server in its own configuration: it then calls complete_task with a note of completion and the task leaves Doing on every device.

Parameters and secrets#

A route can carry parameters — up to 20 names with text values, like branch: main or board: ops. A name is snake_case, starts with a letter and is at most 40 characters; a value is at most 1,000 characters, on one line. A webhook receives them as params; a Mac route puts them in the prompt and the environment. They are visible wherever the route is, so a name that contains a credential word — secret, token, password, passwd, api_key, apikey, bearer, credential, private_key — is refused, max_tokens included: a credential belongs in the route's secret, which is kept apart and never shown again after the moment it is created.

  • A webhook's signing secret — generate one (pws_…), or use your own: 16–200 printable characters, no spaces. Every delegate is then sent with Authorization: Bearer <secret> and X-Cotask-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">, so the receiver can check it is Cotask, and that the body is fresh and untouched.
  • A Mac route's inbound token — prt_…, lets something other than the app queue work for it. See Queue work from anywhere.

Either is shown once — copy it then — and afterwards only as its last four characters. Generating a new one replaces the old at once; removing it revokes it.

A single delegate can add or override parameters, and pick another model or effort for a Mac route — see Delegate a task. Through the API: params, model and effort on POST /delegate. On a send made now, a parameter set to null drops the route's for that send.

Queue work from anywhere#

A Mac route with an inbound token has its own endpoint, hosted by Cotask — a script, a Shortcut or an n8n flow can send work to your Mac with it:

curl -X POST "https://usecotask.com/api/agent/inbound/<route id>" \
  -H "Authorization: Bearer prt_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: nightly-2026-09-26" \
  -d '{"prompt":"Run the test suite and summarise the failures."}'
# 202 {"queued":true,"dispatch_id":"…","dispatch_status":"queued","listener":"online"}

The token also goes in an X-Cotask-Token header, for tools that can't set Authorization.

The body is JSON, at most 64 KB, with only these fields: prompt (up to 20,000 characters) and/or task_id (one of your tasks — its title, notes and attachments go into the prompt), and optionally message (up to 4,000), params and follow_up_of. Anything else is refused.

follow_up_of is the dispatch_id of an earlier request to the same route: the new one is a follow-up, steered into that run while it goes or resuming its session after — {"follow_up_of":"…","message":"Now only the ones touching auth."} is enough. A request with a task_id needs none: it follows up the task's latest run on the route by itself. The answer names the run it follows ("follow_up_of"); another route's run, or none, is 404. The token only works for its own route; only its SHA-256 is stored, and a wrong token, another route's, or a webhook's id all get the same 401.

A token queues work; it doesn't decide how it runs. The route's model, effort, folder (cwd) and access are yours, set on the route: a request that sends model or effort, or a cwd or access parameter — to change it or to clear it — is refused with 403, and one that tries to clear any of the route's parameters with 400. Other parameters are added to the route's, or override them.

A route takes at most 10 new requests a minute (429 past that), whatever their Idempotency-Key. Repeating a key that already queued work returns that work ("duplicate": true) and queues nothing — even over the limit. A key is remembered per route for as long as its run is — seven days after it finishes. GET …/inbound/<route id>/<dispatch id> with the same token says how it went — status (queued, running, done, failed, canceled, expired), exit_code, error, the model and effort, when it was claimed and finished, and for a follow-up the run it follows (follow_up_of) and how it went in (continuation: steered, resumed or fresh) — never the prompt.

Delegate a task#

Open the task → Delegate to AI → pick the route. You can type a line of instructions with it, for when the task alone says too little. Sending starts the task: the app moves it to Today and to Doing, and the send is recorded in History. The toast says where it went — Sent to Claude, or for a Mac route, Queued for … when no Mac is listening.

While it runs, the task shows the agent's mark and Delegated to Claude Code, shimmering — a task you moved to Doing yourself never does, because Cotask remembers who a task was handed to, not just that it is in progress. That memory is the device's: a task sent from the iPhone reads as delegated on the iPhone, and a scheduled send on every device.

From then on the action reads Follow up and goes back to the same agent: same send, but for a task already being worked on — say what changed, what to correct, or what to do next. It returns to Delegate to AI once the task is done.

A follow-up continues the same conversation rather than starting another. On a Mac route, Claude Code or Codex gets it while it is still working — steered into the run, not queued behind it — or, if the run has finished, in the same session, with everything it did before. See Follow-ups on the Mac. A webhook is told the send is a follow-up and which conversation it belongs to (see What arrives).

Options change one send without touching the route: other parameters (for any route), and another model or effort (for Claude Code or Codex — picked from what your Mac's CLI reported, or typed). On the iPhone they're under Options in the delegate sheet — the plain alert has Options… to open it. Names that look like credentials are refused, as they are on the route.

On the Mac: ⌘K → Delegate to AI (Follow up, once it has been sent) — pick the agent, type the line of instructions, ⏎ sends. The same field opens under the buttons on a task's card, where the slider button next to Send opens this send's options: a model and an effort field — with menus of what your Mac's CLI reported — and parameters as one line, branch=main, ticket=PAR-7 (so a value can't hold a comma).

With no route yet, delegating says where to add one: Settings → Delegate to AI on the iPhone, Settings → Agents on the Mac.

Watch, stop or summarise a run#

A task sent to a Mac route shows where its run is — waiting for your Mac, received, working, finished — and tapping that line opens the run: its steps, how it ended, earlier attempts. While it runs:

  • Cancel run stops it. Before a Mac has taken it, it's simply called off; once one has, the Mac hears the request at once, stops the CLI, and records the run as Canceled from Cotask. Either way the task goes back to To Do.
  • Get summary asks the Mac running it how it's going. The Mac reads the run's transcript so far (its log), boils it down to what the agent said and did, and hands that to a small model through the same CLI — Sonnet for Claude Code, GPT-6 Luna for Codex, or the Summary model set in the Delegate Listener pane — in a session of its own, with no tools, settings, hooks or MCP servers, so the run itself is never touched. A few seconds later Progress so far shows the summary; Update summary asks again. Only that Mac has the transcript, so it needs to be listening.

Send it later#

Where: iPhone → Settings → Delegate to AI → Schedule delegate. Mac → Settings → Agents. Off by default: Send means now, as it always has.

Turned on, delegating asks one more thing — when. Now is still the first answer and the one it opens on; next to it are the moments worth a single tap (in an hour, this evening at 18:00 — until it has passed, tomorrow morning and next week at 09:00) and a picker for anything else. Pick one and the button reads Schedule.

The waiting is not the app's. Cotask keeps the send on the server and makes it at the moment you chose, so it goes out with the app closed, the phone asleep, or neither to hand. Until then the task is untouched — it isn't started and nobody has it. It shows a line under its chips:

Going to Claude Code · Tomorrow 09:00 — Send now · Cancel

Send now stops the waiting: the send goes out at once, exactly as it would have at its moment. Cancel while it is still waiting and nothing is sent. When the moment comes, everything a delegate does happens at once: the POST (or, for a Mac route, the queue), the history line, Sending to Claude Code, and — as soon as an app syncs — Today, Doing and Delegated to Claude Code. A send that fails — the webhook refused it, or couldn't be reached — says so on the task with the reason and a Dismiss, and is not retried: a webhook that starts an agent on a task would start it twice. iPhone Settings counts the sends still waiting.

Delegate through the API#

POST /delegate is the same send, for a script of yours signed in as you — the apps use it. Agent keys and connected assistants get 403.

FieldWhat it takes
task_idThe task (required)
webhook_idThe route — webhook or Mac (required)
messageThe line of instructions — trimmed, and cut at 4,000 characters
paramsParameters for this send, added to the route's or overriding them
model · effortAnother model or effort for this send — Mac routes only, 400 on a webhook
send_at · send_inSend later — read on your own clock exactly like a reminder: "2h", "2026-09-01T21:00", or a timestamp with its own zone. A bare number in send_in is minutes. Needs Schedule delegate on; a moment more than a minute gone is refused

What comes back:

{ "delegated": true, "webhook": "Claude", "status": 200, "follow_up": false }
{ "delegated": true, "webhook": "Claude Code", "status": 202, "queued": true,
  "dispatch_id": "…", "dispatch_status": "queued", "follow_up_of": "…", "listener": "online" }
{ "scheduled": { "id": "…", "status": "pending", "send_at": "…", "local_time": "…", "relative": "in 2 hours",
  "message": "…", "webhook": { "id": "…", "name": "Claude" } } }

— a webhook (status is the receiver's; follow_up says whether the task had gone there before), a Mac route (follow_up_of is the run it follows up, when it does), and a send for later. The server sends and records; moving the task to Today and Doing is the apps' part, so a script that wants that sets it with PATCH /tasks/:id.

GET /delegations lists the sends still waiting, each with its status, send_at, local_time, relative, message, webhook, and sent_at and error once it has gone; ?status=all includes the ones that went. DELETE /delegations/:id calls off one that is still waiting.

What arrives#

This is what a webhook receives (a Mac route gets the same task, written as a prompt). A POST with Content-Type: application/json, User-Agent: Cotask/1.0 (delegate), an X-Cotask-Event: task.delegated header and an X-Cotask-Delivery id — plus Authorization and X-Cotask-Signature when the route has a signing secret. A scheduled send is the same POST, made at the moment you picked:

{
  "event": "task.delegated",
  "webhook": { "id": "…", "name": "Claude" },
  "task": {
    "id": "…",
    "title": "Draft the release notes",
    "notes": "…",
    "status": "todo",
    "project": { "id": "…", "name": "Cotask" },
    "due_date": "2026-09-01",
    "priority": "high",
    "recurrence": null,
    "blocked_by": null,
    "reminder": null,
    "completion_note": null,
    "attachments": [
      { "id": "…", "kind": "link", "title": "Spec — Notion", "url": "https://…", "created_by": "user" },
      { "id": "…", "kind": "file", "title": "shot.png", "url": "https://…", "created_by": "user",
        "content_type": "image/png", "byte_size": 184320 }
    ],
    "…": "…"
  },
  "message": "Match the tone of the last one.",
  "params": { "board": "ops" },
  "delegated_at": "2026-08-26T21:14:00.000Z"
}

task is the same object get_task returns — with its reminder, who created it, and when — and project is null when it has none. message is null when you typed nothing. params is there only when the route (or that delegate) has parameters, so a webhook set up before them receives exactly what it always has.

Follow-ups. When the task has gone to this webhook before — a Follow up, a retry, or a scheduled send — the payload also carries follow_up, so the receiver can put it in the conversation it already has instead of starting another. The event is still task.delegated, so a receiver that ignores it keeps working as it did:

"follow_up": {
  "sequence": 2,
  "previous_delivery_id": "…",
  "previous_delegated_at": "2026-08-26T21:14:00.000Z",
  "thread_id": "conv_42"
}

sequence counts the sends of this task to this webhook (2 is the first follow-up), and previous_delivery_id is the earlier send's X-Cotask-Delivery. thread_id is yours: answer any send with JSON carrying a thread_id — {"thread_id": "conv_42"}, up to 200 characters — and Cotask keeps it and sends it back with every follow-up of that task (null until you do). task.id stays the same throughout, so keying on it works too. A first send has no follow_up at all.

To check a signature, recompute it over the raw body:

const [, t, v1] = req.headers["x-parallel-signature"].match(/^t=(\d+),v1=([0-9a-f]{64})$/);
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
const ok = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
  && Math.abs(Date.now() / 1000 - Number(t)) < 300;

Every attachment url is fetchable with no credentials, so the receiving agent can read the brief and its files immediately. Cotask waits up to 20 seconds for a response and reports a failure back to you — a webhook that answers anything other than 2xx shows as an error rather than failing silently.

Give the agent an agent key too, and the loop closes: it receives the task, works, and writes results back onto the same task with complete_task, update_task or attach_file.