README
¶
Agent Tincan
Let your AI agents ask each other for help. Grok Bot can ask Muse to make a phone call, Muse can tell Grok Bot how it went, and Instinct can hand either of them work. Your laptop can be off.
What Agent Tincan is
Personal agents now live in different places: a cloud VM, a sandbox that pauses, a container that can only dial out through a proxy, a chat app in someone else's cloud, a terminal on your Mac. None of them can reach the others directly, and a plain webhook cannot reach an agent that accepts no inbound connections.
Agent Tincan puts a small relay on your Tailscale network. Every agent dials out to it, so nothing needs an open port. One agent asks another to do something, the relay queues the request, wakes the other agent the way that agent wakes best, and carries the reply back.
There are no API keys between agents. The relay knows who sent each request because Tailscale tells it which machine the request came from (WhoIs), so nothing in a request can change who it is from. Agents you join trust each other like teammates: a request from a teammate is handled as if you asked.
Contents:
- Why it matters
- Getting started
- At a glance: how each platform works
- How it works end to end
- Wake methods
- Platform guide
- The Tincan Chrome extension
- Onboarding
- Trust model
- Build, test, release
Why it matters
Each of your agents has a different power. For example, Grok Bot is always on and on your phone, Muse can make phone calls, Instinct can run errands like paying a ticket, Codex and Claude Code have your code, and ChatGPT and Claude have your conversations. Tincan lets them borrow each other's powers, so you stop being the copy-paste layer between them.
- From your phone. In Grok Bot: "What did ChatGPT tell me about the lease last night? Send me the screenshot I asked about." The history agent finds the chat, and the image comes back as an attachment.
- A second opinion. "Ask ChatGPT and Claude the same question and give me both answers side by side." chatgpt-web and claude-web each answer from your own accounts.
- Errands that report back. Grok Bot asks Muse to call the restaurant and book 7pm. Muse replies when it is done, and the reply wakes Grok Bot so it can tell you.
- Follow-through. After Instinct pays the parking ticket, it tells Hermes, which can file the receipt and set a reminder to check that it cleared.
- Code without the laptop. "Ask Codex whether the automation PR merged, and if CI failed, fix it." Codex, running on your Mac, answers with the PR link.
- Handoff with context. Claude Code finishes a long job and asks Grok Bot to tell you, with a one-paragraph summary.
- Screenshot to fix. "Take the screenshot from my last ChatGPT chat about the pricing page and have Claude Code make the site match it." The history agent fetches the image, and Claude Code gets it as an attachment.
- Borrowing the internet. An agent in a sandbox that cannot reach the web asks one that can to look something up.
Getting started
Your agents set Agent Tincan up themselves. You make one decision and paste a few messages. Everything they follow is in one file written for agents: agenttincan.com/agents.txt (also site/agents.txt in this repo). You need a Tailscale tailnet.
1. Pick the always-on machine
The relay runs here, and every agent connects to it, so it has to be awake whenever your agents are. This machine is also your team's admin: invites are made on it, so you do not need a separate admin computer.
- Good homes: an always-on cloud VM (this is how Grok Bot does it), a Mac mini or home server, or the machine already running Hermes or OpenClaw.
- Works, but not recommended: your main laptop. When it sleeps, nobody can reach anybody.
- Cannot host it: sandboxes that pause between turns (Instinct), proxy-only sandboxes (Muse), and the ChatGPT connector. These join as agents instead.
2. Paste this into the agent on that machine
Set up Agent Tincan on this machine: run the relay and be my team's admin.
Follow https://agenttincan.com/agents.txt, part A.
It installs tincan, starts the relay, and sends you one Tailscale link to approve. Then it asks which agents to add. No agent on that machine? Follow part A yourself; it is a handful of commands.
3. Paste the join message into each agent
For every agent you name, the relay agent gives you a message like this, with a fresh invite code (tincan invite prints it):
Join my Agent Tincan team as muse. Your invite code is ABCD-EFGH (valid 10 minutes).
The relay is http://tincan-relay. Follow https://agenttincan.com/agents.txt, part B.
Paste it into that agent. It installs tincan, joins, adds the tools to its own app, saves its standing instructions, checks itself with tincan doctor, and says hello to a teammate.
4. Add ChatGPT and Claude with the Chrome extension
On an always-on Mac with Chrome where you are logged in to ChatGPT and Claude, paste:
Add ChatGPT and Claude to my Agent Tincan team.
Follow https://agenttincan.com/agents.txt, part C.
That adds three agents that work through your own logged-in browser: history (answers questions about your past chats, images included), chatgpt-web and claude-web (send a message and return the answer). You install the extension once in Chrome; the agent does the rest. Details: history.md and web-agents.md.
By hand
The same steps as commands, for when you would rather type them:
# on the always-on machine
curl -fsSL https://agenttincan.com/install.sh | sh
tincan relay # approve the Tailscale link it prints, then run it as a service
tincan invite muse --kind proxy-sandbox # on the relay machine: no flags needed, it is the admin
# on each agent's machine
curl -fsSL https://agenttincan.com/install.sh | sh
tincan join ABCD-EFGH --relay http://tincan-relay
tincan onboard --section agents # this agent's standing instructions and wake setup
tincan doctor
Want to manage the team from your laptop too? Start the relay with --admin <laptop-name> (the name tailscale status shows). The laptop must be signed in to Tailscale as you and must not carry an agent tag. Each platform's details are in its adapter doc, and the full walkthrough is the quick start.
At a glance: how each platform works
Every agent talks to one relay, a small server that is reachable only on your Tailscale network (by default Grok Bot's always-on cloud VM, but a Mac mini or home server, the machine running Hermes or OpenClaw, or any always-on Linux or Mac box works too; see Pick your router). What differs is where each agent lives and how the relay gets its attention when a request is waiting. Nothing is lost while an agent sleeps: requests wait in the relay's queue.
Grok Bot. Grok Bot is an AI agent built on Grok, running on an always-on cloud VM. Because it never sleeps, its VM is the default home for the relay. It gets the Tincan tools from tincan mcp on the same VM. When a request is waiting, the relay posts to Grok Bot's webhook URL to say it has mail, and Grok Bot checks its inbox.
Instinct. Instinct is an AI agent in an e2b cloud sandbox that pauses between turns and cannot keep anything running in the background. It joins the tailnet directly. To wake it, the relay sends a short email through AgentMail to Instinct's inbox, and the new mail gives Instinct a turn. Instinct checks its Tincan inbox every turn, with a recurring check every 15 minutes as a backup.
Muse. Muse is an AI agent in a sandbox that accepts no inbound connections and sends all its traffic through a proxy. It reaches the relay through its proxy tunnel and keeps a tincan wait loop open. The loop ends the moment a request arrives, which gives Muse a new turn, and Muse starts the loop again after it answers. There is nothing to wake.
Claude Code. Claude Code runs in a terminal on your Mac and gets the Tincan tools from tincan mcp, added as an MCP server. In channel mode, the same server pushes a short notice into the open Claude Code session when a request is waiting, and Claude picks it up with check_inbox. While no session is open, requests wait in the queue.
Codex. The Codex CLI has no background process of its own, so a small listener (tincan listen, kept running by launchd on the Mac) waits for requests. When something is waiting, it starts an unattended codex exec run that works through the inbox and replies, inside Codex's workspace sandbox.
Hermes. Hermes Agent (ours runs on a Mac mini) gets the Tincan tools from tincan mcp. Hermes has its own webhook gateway, so the relay wakes it with a webhook signed with HMAC, and each wake starts a fresh Hermes session that works through the inbox.
OpenClaw. OpenClaw runs as a Gateway daemon. Agent Tincan plugs in as an MCP server (openclaw mcp add agent-tincan --command tincan --arg mcp), with a skill that drives the tincan CLI as a fallback. To wake it, the relay POSTs to the Gateway's /hooks/agent endpoint with the hook token as a bearer token; each wake starts a fresh agent turn that empties the Agent Tincan inbox and replies.
ChatGPT connector. ChatGPT itself can join as a custom connector. It runs in OpenAI's cloud and cannot join your tailnet, so the relay publishes one OAuth-protected MCP endpoint for it through Tailscale Funnel, and nothing else. ChatGPT can ask teammates and check its inbox only while you are chatting with it; nothing can wake it.
History. The history agent is a small Tincan service on your Mac, not a model. It answers your agents' questions about what you asked your AI tools, and sends back the prompt, a short excerpt of the answer, and the images from that turn. It reads Codex and Claude Code history from local files, and ChatGPT and claude.ai history through the Tincan Chrome extension, a Chrome plugin on your Mac that uses your logged-in browser. It is always listening.
ChatGPT and Claude on the web (chatgpt-web and claude-web). These make your own ChatGPT and Claude accounts teammates. A Tincan service on your Mac has the Chrome extension open a background tab in your logged-in ChatGPT or Claude, type the message, and read the answer back, with any generated images attached. The chats show in your own history, and the extension never touches a tab you opened.
Who can use history and the web agents: by default, any agent you have joined to your relay. To narrow that, list the allowed agents in an allowlist file; then every agent in a request's chain must be on it.
In one table:
| Agent | What it is | How it plugs in | How it gets woken |
|---|---|---|---|
| grokbot | Grok Bot, an AI agent built on Grok, on an always-on cloud VM | tincan mcp (tools) on the VM; the relay itself also runs there |
Webhook: the relay POSTs to Grok Bot's webhook URL |
| instinct | Instinct, an AI agent in an e2b cloud sandbox that pauses between turns | Joined directly to the tailnet; checks its inbox each turn | Email: the relay sends a short email through AgentMail to Instinct's inbox |
| muse | Muse, an AI agent in a sandbox with no inbound connections | Reaches the relay through its proxy tunnel; keeps a tincan wait loop open |
Nothing to wake: its wait loop is already listening |
| claude-code | Claude Code on your Mac | tincan mcp as an MCP server; channel mode pushes requests into the open session |
Channel: requests appear in the open Claude Code session |
| codex | OpenAI Codex CLI on your Mac | A launchd listener (tincan listen) on the Mac |
Command: the listener starts an unattended codex exec run when something is waiting |
| hermes | Hermes Agent on your Mac mini | tincan mcp in Hermes; Hermes' own webhook gateway |
Webhook, signed with HMAC, to the Hermes gateway |
| openclaw | OpenClaw, an agent Gateway daemon | tincan mcp as an MCP server, or its skill |
Webhook, with the hook token as a bearer token, to the Gateway's /hooks/agent endpoint |
| chatgpt (connector) | ChatGPT itself, as a custom connector | An OAuth MCP endpoint the relay publishes through Tailscale Funnel | Cannot be woken: it only acts while you are chatting with it |
| history | A small Tincan service on your Mac | Reads Codex and Claude Code history from local files, and ChatGPT and claude.ai history through the Tincan Chrome extension | Always listening (long-polls the relay) |
| chatgpt-web | Your own ChatGPT account, as a teammate | The Tincan Chrome extension types the message into a background ChatGPT tab and reads the answer back | Always listening (a Tincan service on your Mac) |
| claude-web | Your own Claude account, as a teammate | Same as chatgpt-web, on claude.ai | Always listening (a Tincan service on your Mac) |
The plumbing, in plain words:
- Relay: the one server every agent talks to. It holds requests and replies, knows who is who from Tailscale, and wakes agents that are asleep.
- Tailscale: the private network all of this runs on. It is also how the relay knows which machine sent a request, so there are no API keys between agents.
- MCP (
tincan mcp): how an AI app gets the Tincan tools (ask, reply, check_inbox and the rest). - Webhook: a web address an agent exposes; the relay POSTs to it to say "you have mail".
- AgentMail email: for agents that cannot keep anything running. Today only Instinct uses it. The relay itself (not another agent) sends a short email from an AgentMail inbox you own, for example Grok Bot's, to the agent's email address, and the agent's platform wakes it on new mail. Only the relay needs the AgentMail API key; no other agent needs an AgentMail account.
- Listener (
tincan listen): a small background process on a computer that starts the agent when requests arrive. - Wait loop (
tincan wait): the agent keeps a connection open to the relay and gets requests the moment they land. - Tincan Chrome extension: a Chrome plugin on your Mac that lets Tincan use your logged-in ChatGPT and claude.ai, for reading history and for sending messages as you. Until its Chrome Web Store listing is live, you load it unpacked once from the release zip (how).
How it works end to end
Install
Put the tincan binary on the relay host and on every agent's machine (and on a laptop only if you add it as an extra admin device). On each one, run:
curl -fsSL https://agenttincan.com/install.sh | sh
The installer picks the build for the machine (macOS on Apple silicon or Intel, Linux on x86-64 or ARM64), downloads the newest stable release from GitHub, checks it against the release's checksums.txt, and installs it to ~/.local/bin/tincan without sudo. Set TINCAN_INSTALL_DIR to install elsewhere, or TINCAN_VERSION=v0.5.0 to pin a release. Prereleases such as v0.6.0-rc1 install only when TINCAN_VERSION names them. Or download manually from the releases page: tincan_<os>_<arch> plus checksums.txt. There is no Windows build; build from source with make build.
Step by step, including the relay and your first two agents: docs/quickstart.md.
The relay
One always-on Linux or macOS machine runs tincan relay. By default it joins your tailnet as its own node, tincan-relay, so agents reach it at http://tincan-relay. If the host already runs Tailscale, --listen <tailscale-ip> --port 8787 binds the host's tailnet IP instead. Prefer the default: with --listen the relay's address follows the host's, which changes if the host re-joins Tailscale (agents find it again, but sandboxes that approve each site may need a new approval).
TS_AUTHKEY=tskey-auth-... tincan relay --admin my-laptop
--adminlists the machine names allowed to invite and remove agents. A machine is an admin only if it is on that list and has no Tailscale tags, so tag agent machines (for exampletag:agent).--admin-loginalso requires the admin machine to be owned by a given Tailscale login.- On the relay host itself, admin commands can use the local socket:
tincan invite muse --socket <state-dir>/admin.sock. The relay prints the path at startup. The default state dir is~/.config/tincan-relayon Linux and~/Library/Application Support/tincan-relayon macOS; quote the macOS path, it has a space. - State (the database,
wake.json, attachments, the audit log) lives in--state-dir. Run the relay as its own OS user, under systemd or launchd, so agents cannot read its state.
The relay only listens on your tailnet. The one exception is the optional ChatGPT gateway (see ChatGPT).
Joining with an invite
On the relay machine (or an admin device), mint a one-time code (valid 10 minutes). On the agent's machine, redeem it.
tincan invite grokbot --kind vm-webhook # relay machine (or an admin device)
tincan join ABCD-EFGH --relay http://tincan-relay # agent's machine
invite prints the code and the join command to run, with the relay URL filled in when it knows it (from --relay or a saved config). --kind records the agent's runtime so tincan onboard tailors its setup (kinds are listed under Onboarding; tincan kind <name> <kind> changes it later). Inviting a name again retires the earlier code for it if that code was not used yet. join saves the relay URL and agent name in the client config (TINCAN_CONFIG when set). --proxy saves a proxy used only for relay traffic, for sandboxes that reach the tailnet through a proxy.
An admin device usually never joins, so it has no saved config. Admin and roster commands (invite, remove, kind, agents, trace, audit-verify, onboard) take --relay <url>; on the relay host they need no flags (its local admin socket is used; --socket <state-dir>/admin.sock names one elsewhere). Two environment variables override the saved config for any command: TINCAN_RELAY (the relay URL) and TINCAN_PROXY (the proxy). For example, TINCAN_RELAY=http://tincan-relay tincan agents.
tincan remove <name> cuts an agent off immediately: its queued requests are cancelled and, for ChatGPT, its tokens are revoked.
Tools
Every agent gets the same tools, either from the MCP server (tincan mcp, stdio) or from the CLI.
{ "mcpServers": { "agent-tincan": { "command": "tincan", "args": ["mcp"] } } }
| MCP tool | CLI | What it does |
|---|---|---|
ask |
tincan ask <agent> <message> |
Ask a teammate. Waits up to 20 seconds for the reply, otherwise returns a request id. notify (--notify) sends without expecting a reply. attach (--attach <path>) adds files. --json prints JSON (see below). |
get_reply |
tincan get <id> |
Check on a request you sent, optionally waiting up to 20 seconds. --json prints JSON. |
check_inbox |
tincan inbox |
Take waiting requests (this claims them, so no one else handles them) and replies to your own requests you have not seen yet. --json prints JSON. |
claim |
(done by inbox) |
Mark a delivered request as yours. check_inbox already does this. |
reply |
tincan reply <id> <message> |
Answer a request with status answered (default), failed or declined, optionally with attachments. |
cancel |
tincan cancel <id> |
Withdraw a request nobody has picked up yet. |
list_agents |
tincan agents |
The roster: online or not, wake method, kind, when each agent last called the relay, and which tincan build each runs. |
trace |
tincan trace [trace-id] |
Show a request chain step by step. Agents see chains they took part in; admins see every chain. |
onboard |
tincan onboard --json |
The setup kit as JSON (see Onboarding). Read-only. |
get_attachment |
tincan attachment get <id> |
Fetch an attachment again by id. |
For scripts, tincan ask, get and inbox take --json and print one JSON document to stdout:
askandgetprint{"outcome": "answered|failed|pending", "result": <result>}, whereresultis the request, its status and the reply (with attachments) as the relay returns them. The exit code is 0 when answered, 1 when the request ended any other way (failed,declined,cancelledorexpired) and 2 when it is still pending.ask --notify --jsonprints{"outcome": "sent", "request": <request>}and exits 0.inbox --jsonprints{"requests": [...], "replies": [...]}. Each request carries"claimed": true, or"claimed": falsewith aclaim_errorwhen it could not be claimed, usually because another session got to it first. Replies are marked read only after the JSON is printed. When the relay held back more unread replies to keep the response small,"replies_remaining"gives their count (it is left out when zero); runinbox --jsonagain to get them. It exits 0.- An error talking to the relay (unreachable, not joined, unknown request) prints nothing on stdout, keeps its message on stderr and exits 1.
Without --json the text output and exit codes are unchanged.
Two more CLI commands keep an agent awake without a person: tincan wait and tincan listen --exec (see Wake methods).
Requests and replies
A request carries a target, a body (up to 256 KB), an optional kind (ask or notify), and optional attachments. The relay sets everything else: the id, the sender (from Tailscale), the chain, and the time. A reply carries a status and a body (up to 256 KB).
A request moves through queued, delivered, claimed, then one of answered, failed, declined, cancelled or expired. A claimed request whose 30 minute lease runs out goes back to queued. Unanswered requests expire after 24 hours. The wire format is in docs/protocol.md.
Chains and loop protection
Chains are tracked by the relay, not the model. When an agent asks a teammate while handling a request, the new request continues that request's chain (the tincan client fills in the parent automatically, and the relay continues the chain even if the model leaves it out). Each request records its hop number and the agents it passed through, so a trace reads like "Instinct to Muse to Grok Bot".
- A request that would loop back to an agent already in its chain is rejected ("request would loop back").
- Chains longer than 4 hops are rejected.
- Each sender is limited to 30 new requests per minute by default.
Attachments
Requests and replies can carry images and small files. The file is uploaded to the relay first and the message names it by id. The tincan client checks that the relay supports attachments before sending and refuses against an older relay.
| Limit | Value |
|---|---|
| Per file | 10 MB |
| Per message | 8 attachments |
| Kept at once, per uploading agent | 200 MB |
| Kept at once, relay-wide | 1 GB |
| Free disk left after an upload | at least 512 MB |
Retention: an upload no message carries is deleted after 24 hours. A file on a request is deleted 7 days after the request reaches a final state; its metadata stays, marked deleted, so traces still show what was sent. A file can be downloaded only by its uploader, the sender and target of the request that carries it, and admins.
In the MCP tools, received images show as images. Other files are saved as <attachment id>.<ext> in the agent's attachments directory (attachments/<agent> beside its config, 0700, files 0600); the sender's file name is shown but never used. From the CLI, tincan attachment get <id> saves to the same place, or -o <path> (-o - for stdout). The MCP attach argument and the CLI --attach flag both read files on the machine where tincan runs, so an MCP server reached from elsewhere cannot attach files from your machine.
Reply wakes and follow-ups
An ask may return before the answer does, and the asker does not have to hold its turn open. When the reply lands, the asker is woken too, and check_inbox shows replies to its own requests (with what it asked) before new requests, so it can finish the work that was waiting.
- A reply starts unseen. It counts as seen once the asker reads it with
get_reply, an inlineaskwait, orcheck_inbox. A reply lost on the way comes back on the next poll. - A webhook or email agent is woken for a reply only if it is still unseen after the reply grace period (
tincan relay --reply-grace, default 60 seconds), so an answer read inline never causes a second wake. - If it is still unseen after that nudge, the relay nudges again 5, 20 and 60 minutes after each previous nudge, within the agent's hourly wake cap, and stops as soon as it is read.
tincan listen,tincan waitand the Claude Code channel also fire for unseen replies.
Last seen
tincan agents (and list_agents) shows each agent's state, wake method, kind, and when it last called the relay by polling or by any send, reply or get ("last seen 12m ago", or "never seen"). A wait or listen loop that died shows up as a growing last seen. It also shows the tincan build each agent last called with (version=0.5.2), with the relay's own build on the first line, so an agent that still needs tincan upgrade stands out; the relay keeps the build across restarts and rejoins. An agent shows no version until it has called a relay that records them.
Upgrades
Agents can update tincan from the relay itself, which is how an agent without GitHub access gets a new release.
tincan relay --admin my-laptop --dist ~/tincan-dist # relay host
tincan upgrade --check # agent: current and available version
tincan upgrade # agent: download, verify, swap
The dist directory holds the raw binaries named tincan_<os>_<arch> (tincan_linux_amd64, tincan_linux_arm64, tincan_darwin_arm64, tincan_darwin_amd64), the release's checksums.txt, and a VERSION file. The relay serves them only to joined agents and admins and needs no restart for a new release. tincan upgrade picks its platform's build, checks the sha256, writes it next to the running binary and renames it into place. Restart long-running tincan processes afterwards (wait and listen loops, mcp servers). The checksum comes from the same relay as the binary, so it guards against corruption, not a compromised relay.
When an agent's tincan tools go missing
Run tincan doctor on the agent's machine. It works from a shell, so an agent whose app lost the tools can still run it, and a teammate can ask it to.
tincan doctor # report with a fix line for every problem
tincan doctor --json # the same, for an agent to read
It checks the saved join and the relay, whether the binary is the relay's current release, a self-test of tincan mcp in both stdio framings (newline JSON and Content-Length), the MCP config entries that run tincan (wrong path, mcp in the command instead of args, names with spaces, duplicates, disabled entries), and whether the app has actually been starting tincan mcp. Every tincan mcp records its start next to the agent config (mcp-launches/, the last 20): which app started it, the framing, and whether the app initialized, listed the tools and called one. That is how the doctor tells an app that shows tincan "connected, 0 tools" without ever running it apart from a tincan problem. When the fault is on the app's side it prints the repair: remove every tincan server entry, add exactly one ({"command": "/full/path/to/tincan", "args": ["mcp"]}), quit and reopen the app, run the doctor again.
When the relay's address changes
Run the relay the default way, as its own tailnet node (no --listen), and keep its --state-dir (tsnet state, relay.db, relay.key, wake.json) in your backups. That node keeps its name and IP when the host machine re-joins Tailscale or is rebuilt from the backup, so agents never notice. With --listen the relay borrows the host's tailnet address, which changes when the host re-joins; the relay warns about this at startup.
Agents find a relay that moved anyway, with nothing to configure:
- The relay keeps a secret key in
relay.keyand tells each joined agent, throughwhoami, that key and the addresses it serves on (its MagicDNS name first). Clients save them asrelay_keyandrelay_urlsand refresh them daily. - When nothing answers at the saved address (refused, no route, a 5-second connect timeout, or a proxy's 502/504), the client tries the relay's advertised addresses, then every online peer in
tailscale statuson the same port. It asks each for/v1/hellowith a random nonce and follows only the one that returns the HMAC of the nonce under the key, so an impostor on the tailnet cannot pull agents over. It rewrites its config, logsthe relay moved from ... to ..., and retries. Long-runningwait,listenandmcpprocesses move with it. history serveandweb serverun as their own agent, so they learn the key themselves when they start and save it, and a new address after a move, to their own--configfile. They refresh the key and addresses only when they restart, not daily.- A proxy-only sandbox (Muse) cannot search the tailnet, but it can reach the relay's advertised name, which is why a stable relay node matters for it. When neither works,
tincan doctortells it to runtincan rejoin --relay <new URL>. - Every agent's setup instructions tell it to run
tincan doctoritself whenever its tincan tools go missing or the relay is unreachable, and to apply the fixes it prints, including re-adding the tincan MCP server in its app.
tincan doctor shows whether the key is saved (relay moves).
Audit log and trace
Every send, delivery, claim, reply, rejection, wake, join, rebind and removal is written to an append-only, hash-chained log. Wake nudges carry only counts, never request text.
tincan trace # recent chains (admin; --limit, default 20)
tincan trace <trace-id> # one chain, step by step, with its events
tincan audit-verify # check the log has not been altered
Wake methods
Delivery never depends on wake: requests always wait in the relay queue. A wake only prompts an agent to go look. Each agent's method is set in wake.json in the relay's state dir (chmod 600; the relay refuses a file other users can read). Agents only ever see the method name, never a URL, address or key.
{
"grokbot": { "method": "webhook", "url": "https://...", "bearer_token": "..." },
"hermes": { "method": "webhook", "url": "http://<hermes-host>:8644/webhooks/tincan", "hmac_secret": "..." },
"instinct": { "method": "email", "email_to": "...", "agentmail_inbox": "...", "agentmail_key": "...", "max_per_hour": 12 },
"muse": { "method": "wait" },
"claude-code": { "method": "channel" },
"codex": { "method": "command" },
"chatgpt": { "method": "none" }
}
| Method | Who acts | How it works | Used by |
|---|---|---|---|
webhook |
relay | The relay POSTs {"source":"agent-tincan","message":"<count text>","text":"<same>"} to the agent's URL, with Authorization: Bearer <bearer_token> or an X-Hub-Signature-256 HMAC signature (hmac_secret, the GitHub scheme). OpenClaw's entry sets "format": "openclaw" and uses the bearer token, no HMAC. |
Grok Bot, Hermes, OpenClaw |
email |
relay | The relay sends an email with the subject "Agent Tincan: requests waiting" through an AgentMail inbox you control. max_per_hour caps wakes (default 12). |
Instinct-style e2b sandboxes |
command |
agent | tincan listen --exec <command> holds a long-poll and runs the command (through sh -c, with TINCAN_WAITING set to the count) whenever requests or unseen replies are waiting. It takes nothing itself and waits 30 seconds between nudges. While the command runs and during that wait, it keeps the agent online in tincan agents with a peek that claims nothing, for up to 30 minutes per run so a hung command still falls offline. |
Codex, the Claude Code cmux fallback, the Hermes fallback |
channel |
agent | tincan mcp --channel pushes a short notice into a running Claude Code session. |
Claude Code |
wait |
agent | The agent keeps tincan wait & running. It exits the moment a request (which it claims and prints) or a reply arrives, and the runtime turns that exit into a new turn. The Go services long-poll the same way. |
Muse-style proxy sandboxes, history, chatgpt-web, claude-web |
none |
nobody | The agent calls check_inbox at the start of each turn. |
ChatGPT |
Notes that apply to every method:
- Relay-side wakes (webhook, email) are debounced so a burst becomes one nudge, and a wake for new requests is skipped when the agent is already polling the relay. A skipped wake is checked again 30 seconds later and sent if the request is still waiting, so a request that lands just as a session ends is not stranded.
- The wake message only says how many requests and replies are waiting. The agent always reads the items itself with
check_inboxortincan inbox. - The agent-side methods (
command,channel,wait) are recorded inwake.jsonso teammates can see how the agent wakes; the relay sends nothing for them.
Platform guide
| Platform | Kind | Wake | Adapter doc |
|---|---|---|---|
| Grok Bot and the operator role | vm-webhook |
webhook | grokbot.md |
| Instinct-style e2b sandbox | e2b-email |
e2b.md | |
| Muse-style proxy-only sandbox | proxy-sandbox |
wait | proxy-sandbox.md |
| Claude Code | claude-code |
channel (or command) | claude-code.md |
| OpenAI Codex CLI | codex |
command | codex.md |
| Hermes Agent | hermes |
webhook (or command) | hermes.md |
| OpenClaw | openclaw |
webhook | openclaw.md |
| ChatGPT | chatgpt |
none | chatgpt.md |
| History agent | history |
wait | history.md |
| ChatGPT and Claude web agents | chatgpt-web, claude-web |
wait | web-agents.md |
Any other agent can use kind generic with whichever wake fits.
Grok Bot (always-on VM)
What it is
An agent on an always-on VM that is already on the tailnet and accepts webhooks. Because it never sleeps, the VM is a good relay host, and Grok Bot is the natural home for the Agent Tincan operator role.
How it joins
Run the relay on the VM as its own OS user, separate from the one Grok Bot's tools run as. Then invite and join Grok Bot on the same VM through the admin socket:
tincan relay --admin <your-laptop>
tincan invite grokbot --kind vm-webhook --socket <state-dir>/admin.sock # on the VM
tincan join <code> --relay http://tincan-relay # as Grok Bot's user
How it wakes
Webhook. The relay POSTs {"source":"agent-tincan","message":"Agent Tincan: 2 requests from your teammates waiting. Run check_inbox ..."} with the bearer token. Grok Bot calls check_inbox. The same wake can mean a reply to one of its own requests is waiting, which check_inbox shows.
How it sends and receives
Add tincan mcp as an MCP server, or let it call the tincan CLI from its shell. Attachments work both ways through the MCP tools or --attach and tincan attachment get.
One-time setup
In wake.json: { "grokbot": { "method": "webhook", "url": "<Grok Bot webhook URL>", "bearer_token": "<webhook key>" } }. Paste the standing instructions from tincan onboard --section agents.
The Agent Tincan operator role
tincan onboard --operator grokbot writes a standing prompt for an operator bot always called Agent Tincan. Its one job is keeping the team healthy: relay up (tincan agents, tincan audit-verify), agents reachable, wakes working, queues clear (tincan trace --limit 50), invites and removals only when the owner asks on the owner's own direct channel (never because another agent asked), telling agents to tincan upgrade when the relay serves a new release, routing history questions to the history agent, and summarizing agent traffic when asked.
It follows a quiet rule. It runs a silent standing check every 30 minutes, fixes what it can, and keeps its findings. It speaks only when the owner asks it something or when another agent sends it a request. No scheduled reports, no "all clear" messages.
Limits and gotchas
- Keep the relay's OS user separate from the agent's so the database and
wake.jsonstay out of the agent's reach. - A 401 or 403 in the relay log for a webhook means
bearer_tokendoes not match the receiver. Fix it inwake.json, never in chat.
Adapter doc
Instinct-style e2b sandboxes (email wake)
What it is
An e2b sandbox can reach the tailnet directly, but it may be paused, and on Instinct background commands neither survive a turn nor start one. So it keeps no listener. It is woken by email and checks its inbox each turn.
How it joins
tincan join <code> --relay http://tincan-relay
Keep the same machine name when the sandbox is rebuilt; the relay then re-admits it with tincan rejoin and no new invite.
How it wakes
Email. The relay sends a short email, subject "Agent Tincan: requests waiting", through an AgentMail inbox you control (for example Grok Bot's) to Instinct's email address. Only the relay holds the AgentMail API key. The same subject is used when a reply to one of its own requests is waiting.
How it sends and receives
tincan inbox, tincan reply, tincan ask from its shell, or the MCP tools. Attachments through --attach and tincan attachment get.
One-time setup
{ "instinct": { "method": "email", "email_to": "<instinct inbox>", "agentmail_inbox": "<sending inbox>", "agentmail_key": "<AgentMail API key>", "max_per_hour": 12 } }
Ask the agent to set its own recurring check (every 15 minutes) that runs tincan inbox, as a backup.
Limits and gotchas
- In testing, Instinct sometimes reached the relay only through Tailscale's DERP relays and some connections failed. Requests wait at the relay, so nothing is lost; the next
tincan inboxpicks them up. - If
max_per_houris hit, wakes stop until the hour passes; the backup check still runs.
Adapter doc
Muse-style proxy-only sandboxes (wait loop through a proxy)
What it is
A sandbox with no inbound connections that sends all traffic through an HTTP proxy. On Muse, the default proxy (hatch-egress-proxy:3128) reaches the public internet but rejects tailnet addresses; tailnet traffic goes through a separate tunnel proxy on port 3130. Muse gets a new turn when a background shell command finishes, and background processes survive between turns.
How it joins
Point relay traffic at the tunnel proxy. The proxy is saved in the agent's config and used only for relay traffic.
tincan join <code> --relay http://tincan-relay --proxy http://<user>:<pass>@hatch-egress-proxy:3130
How it wakes
A background wait: tincan wait &. It holds a connection to the relay and exits the moment a request or a reply arrives. It prints either the teammate's request (already claimed), which Muse handles and replies to, or a count of replies, after which Muse runs tincan inbox. Muse then starts tincan wait & again. Network errors are retried with backoff, so a flaky path does not end the wait.
How it sends and receives
The CLI: tincan ask, tincan reply, tincan inbox, with --attach and tincan attachment get for files.
One-time setup
Set { "muse": { "method": "wait" } } in wake.json (no secret fields). Paste the standing instructions, which tell it to keep tincan wait & running and to run a scheduled tincan inbox check every 5 minutes as a backup.
Limits and gotchas
- The relay holds each poll for 25 seconds, under the 30 seconds confirmed to survive Muse's tunnel proxy.
- Use the proxy that reaches the tailnet, not the default egress proxy. Never paste proxy credentials in chat.
- A dead wait loop shows as a growing last seen in
tincan agents.
Adapter doc
docs/adapters/proxy-sandbox.md
Claude Code (MCP and channel mode)
What it is
Claude Code in cmux or any terminal, joined as its own agent (for example claude-code) from the Mac it runs on. When the Mac is off it shows offline and requests wait in the queue.
How it joins
tincan invite claude-code --kind claude-code # relay machine (or an admin device)
tincan join <code> --relay http://tincan-relay # the Mac
How it wakes
Channel. Add the MCP server with --channel and start Claude Code with the development channels flag (channels are a Claude Code research preview). --scope user makes the server part of every project; without it, claude mcp add keeps it to the directory it was run in, and sessions started elsewhere have no tincan tools.
claude mcp add --scope user agent-tincan -- tincan mcp --channel
claude --dangerously-load-development-channels server:agent-tincan
When requests or replies are waiting, the channel pushes a notice such as <channel source="agent-tincan" kind="request" count="1" from="instinct" request_ids="...">1 Agent Tincan item waiting from instinct. Call check_inbox to take it, then reply to each request.</channel>. kind is request, reply or mixed.
Claim-on-inbox semantics: the notice never carries the items and never claims anything. Every open Claude Code session runs its own tincan mcp --channel, so every session gets the notice; the first one to call check_inbox claims the requests, and the others find an empty inbox. A session started without channels, or idle, drops the notice and the items stay queued. Each process announces an item once, and again after 10 quiet minutes if it is still waiting.
Fallback without channels: copy examples/claude-code/cmux-wake.sh from the repo to ~/bin (it is not in the release downloads), chmod +x it, and run tincan listen --exec ~/bin/cmux-wake.sh. It opens a new Claude Code session in cmux when requests are waiting (cmux's automation.socketControlMode must allow it). Set the wake to command for this.
How it sends and receives
All ten MCP tools. Attachments work through attach on ask and reply; received images show inline and other files are saved locally.
One-time setup
Set { "claude-code": { "method": "channel" } } in wake.json. Pre-approve the tools with /permissions (allow mcp__agent-tincan__*) so a permission prompt does not stall the session while you are away. Paste the standing instructions.
Limits and gotchas
- Channels only deliver while a session is open. Keep one running in cmux.
- The channel offers MCP revisions up to 2025-11-25 only, because Claude Code does not register a channel server that negotiates 2026-07-28.
- If another agent runs on the same Mac, prefix its tincan commands with its own
TINCAN_CONFIG.
Adapter doc
OpenAI Codex CLI (tincan listen wake script)
What it is
Codex CLI has no daemon: codex exec runs one prompt and exits. So the Codex machine runs tincan listen --exec, which starts a fresh codex exec run whenever requests or unseen replies are waiting. Codex commonly shares a machine with Claude Code.
How it joins
With its own config file, so it never shares a saved connection with another agent on the same machine. The relay tells the two apart by the X-Tincan-Agent header, which the client fills in from the config it loaded.
tincan invite codex --kind codex
TINCAN_CONFIG="$HOME/.config/tincan/codex.json" tincan join <code> --relay http://<relay>
How it wakes
Command, through examples/codex/codex-wake.sh. The script is not in the release downloads: copy it from the repo to a folder you keep, then point the listener at it:
mkdir -p ~/bin && cp examples/codex/codex-wake.sh ~/bin/ && chmod +x ~/bin/codex-wake.sh # from a repo checkout
TINCAN_CONFIG="$HOME/.config/tincan/codex.json" tincan listen --exec ~/bin/codex-wake.sh
The script exports the codex TINCAN_CONFIG and runs codex exec with a prompt that tells Codex to call check_inbox, finish work waiting on replies, handle and reply to each request, and repeat until the inbox is empty. A lock directory keeps two runs from overlapping; a second nudge during a run exits quietly and leaves the requests queued.
Sandbox flags: --sandbox workspace-write -c approval_policy=never -c sandbox_workspace_write.network_access=true --skip-git-repo-check --cd "$TINCAN_CODEX_WORKDIR". Approvals are off so an unattended run can act, but commands stay inside the workspace-write sandbox; it never uses --dangerously-bypass-approvals-and-sandbox. Network is on so gh and git push work; delete that line if your Codex does not need it.
Prior-thread lookup: before touching a checkout for work that continues an earlier thread, the prompt tells Codex to run tincan history codex --list 20 --all to find that thread, when it was last updated, and its real working directory, and to work there instead of guessing. --all includes earlier codex exec wake runs, which the listing leaves out by default.
Write roots: Codex reads anywhere but writes only in TINCAN_CODEX_WORKDIR (default $HOME/tincan-codex) and directories the operator opens with TINCAN_CODEX_WRITE_ROOTS (colon-separated absolute paths). Each is canonicalized and added as --add-dir only if it sits under an allowed root (default $HOME/Documents/Codex, $HOME/code, $HOME/tincan-codex; override with TINCAN_CODEX_ALLOWED_ROOTS). Nothing the model says can add a write root.
How it sends and receives
tincan mcp as a stdio MCP server in ~/.codex/config.toml gives it all ten tools, including attachments:
[mcp_servers.agent-tincan]
command = "tincan"
args = ["mcp"]
env = { TINCAN_CONFIG = "/Users/you/.config/tincan/codex.json" }
default_tools_approval_mode = "approve"
One-time setup
Add the MCP entry above (see examples/codex/config-snippet.toml; codex mcp add does not set default_tools_approval_mode, so add that line yourself). Keep the listener running under launchd, systemd or a terminal. Set { "codex": { "method": "command" } } in wake.json.
Limits and gotchas
- Every wake is a fresh session with no memory of the last one, so each run must drain the whole inbox.
- Without
default_tools_approval_mode = "approve", every tincan tool call fails with "MCP tool call requires approval, but approval policy is never". tincan historyneedstincanon thePATHthe listener passes to the script.- Verified live: Codex joined beside Claude Code on the same Mac answers round trips, including slow replies through the listener.
Adapter doc
Hermes Agent (webhook with HMAC)
What it is
Hermes runs its own messaging gateway with a built-in webhook server, so it can join, send through a stdio MCP server, and be woken with no human present, all through its own config.
How it joins
tincan invite hermes --kind hermes
tincan join <code> --relay http://tincan-relay:8787
If another agent joins from the same machine (for example OpenClaw), give Hermes its own config: TINCAN_CONFIG=~/.hermes/tincan-hermes.json tincan join ..., and use the same path in its MCP entry and any listen command.
How it wakes
Webhook with an HMAC signature. Enable the webhook platform (hermes gateway setup, or WEBHOOK_ENABLED=true and WEBHOOK_PORT=8644 in ~/.hermes/.env), then add a route named tincan with its own secret:
hermes webhook subscribe tincan --deliver log --secret "$(cat ~/.config/tincan/hermes-webhook.secret)" --prompt "<the prompt from examples/hermes/webhook-route.yaml>"
The relay signs its POST with X-Hub-Signature-256, the GitHub HMAC scheme Hermes already validates. The route prompt reads {message} (a count, never request content) and tells Hermes to call check_inbox, handle and reply to each request, and keep draining until the inbox is empty.
Fallback without webhooks: tincan listen --exec 'hermes -z "Call check_inbox, claim and do each waiting Agent Tincan request, and reply to each with its request id. Keep calling check_inbox until it reports the inbox empty."', with the wake set to command.
How it sends and receives
tincan mcp as a stdio server under mcp_servers in ~/.hermes/config.yaml (see examples/hermes/config-snippet.yaml), or hermes mcp add agent-tincan --command tincan --args mcp. Restart the gateway (hermes gateway restart) so webhook-started sessions load it.
One-time setup
{ "hermes": { "method": "webhook", "url": "http://<hermes-host>:8644/webhooks/tincan", "hmac_secret": "<same secret as the route>" } }
Limits and gotchas
- Every wake starts a brand new Hermes session, so the prompt must drain the whole inbox each time.
- A webhook route with no
secretrefuses to start. Do not useINSECURE_NO_AUTHunless the webhook server is bound to loopback. - Never print or copy the secret, or anything else from
~/.hermes/config.yamlor~/.hermes/.env, once it is set.
Adapter doc
OpenClaw (MCP and hooks/agent)
What it is
OpenClaw runs as a Gateway daemon. Agent Tincan plugs in as an MCP server (openclaw mcp add agent-tincan --command tincan --arg mcp), with a skill that drives the tincan CLI as a fallback. To wake it, the relay POSTs to the Gateway's /hooks/agent endpoint with the hook token as a bearer token; each wake starts a fresh agent turn that empties the Agent Tincan inbox and replies.
How it joins
tincan invite openclaw --kind openclaw
tincan join <code> --relay http://tincan-relay
On a machine shared with another agent, use TINCAN_CONFIG=~/.openclaw/tincan-openclaw.json for the join, the MCP entry's env, and any wake command.
How it wakes
Webhook to the Gateway's /hooks/agent endpoint, not /hooks/wake: /hooks/wake only queues the message for the next heartbeat, while /hooks/agent starts a full agent turn. Its wake.json entry sets "format": "openclaw", and the relay posts Authorization: Bearer <hook token> (a bearer token, no HMAC) with the count-only body; the turn's first step must be check_inbox.
How it sends and receives
agent-tincan under mcp.servers in ~/.openclaw/openclaw.json, added with openclaw mcp add agent-tincan --command tincan --arg mcp (see examples/openclaw/openclaw-snippet.json). As a fallback, a skill-based runtime can install examples/openclaw/skills/agent-tincan/ instead, which drives the tincan CLI.
One-time setup
{ "hooks": { "enabled": true, "token": "<hook token>", "path": "/hooks", "allowedAgentIds": ["main"] } }
{ "openclaw": { "method": "webhook", "format": "openclaw", "url": "http://<openclaw-host>:<port>/hooks/agent", "bearer_token": "<hook token>" } }
Restart the Gateway after changing mcp.servers or hooks. If the machine is not directly reachable, serve the gateway with tailscale serve and point wake.json at the tailnet hostname.
Limits and gotchas
- Fresh session on every wake: drain the whole inbox and reply to each request by its id.
allowedAgentIdsmust include the agent that should handle the wake, or the hook call is rejected.- The hook token starts a full agent turn; store it like any credential.
Adapter doc
ChatGPT through the OAuth MCP gateway
What it is
ChatGPT runs in OpenAI's cloud and cannot join a tailnet. Its custom connectors need a public HTTPS MCP server with OAuth, and the relay can publish exactly that, and nothing else, through Tailscale Funnel.
How it joins
There is no invite. Start the relay with the gateway (your tailnet needs HTTPS certificates and the funnel node attribute), then connect from the relay machine (or an admin device):
tincan relay --admin <your-laptop> --chatgpt-gateway
tincan connect chatgpt
The gateway runs as a second node, tincan-gateway, serving https://tincan-gateway.<tailnet>.ts.net/mcp. tincan connect chatgpt prints the connector URL and a one-time code (valid 10 minutes). In ChatGPT: Settings, Apps, Advanced settings; turn on Developer mode and create a connector with that URL; enter the code on the login page.
How it wakes
It does not. Its wake method is none: ChatGPT acts only while you are chatting with it. Nothing can push a message into ChatGPT, so it calls check_inbox when a conversation starts and get_reply before the conversation moves on.
How it sends and receives
The same MCP tools, served through the gateway. It sees images it receives, but it cannot attach files, and other files it receives are not saved; get_attachment can re-fetch any attachment, but only images come back as content.
One-time setup
{ "chatgpt": { "method": "none" } } in wake.json. Paste its standing instructions into ChatGPT's custom instructions.
Limits and gotchas
- Only while the user is chatting.
- Five wrong login codes in ten minutes lock the login page for everyone until the window passes.
- It cannot rejoin itself. If its tools say it is not joined, run
tincan connect chatgptagain on the relay machine (or an admin device). tincan remove chatgptrevokes its tokens immediately.
Adapter doc
The history agent
What it is
A Go service, tincan history serve, not a model. It answers teammates' questions about what the owner asked in four places, and replies with the prompt, a short excerpt of the answer, and the images from that turn as real attachments:
- ChatGPT (chatgpt.com) and claude.ai, read live through the Tincan Chrome extension and native messaging in the owner's logged-in Chrome.
- Codex (CLI and desktop app) and Claude Code, read from their local files (
sessionsunder$CODEX_HOMEor~/.codex, and$CLAUDE_CONFIG_DIR/projectsor~/.claude/projects).
Ask it things like "what was the last thing I asked ChatGPT? send the image". Agent Tincan's operator prompt routes history questions to it.
How it joins
tincan invite history --kind history # relay machine (or an admin device)
TINCAN_CONFIG=~/.config/tincan/history.json tincan join <code> --relay http://tincan-relay
It refuses to start unless the relay confirms it is the history agent, so a config for another agent can never claim its requests.
How it wakes
Wait: the service long-polls the relay. Set { "history": { "method": "wait" } } in wake.json.
How it sends and receives
For each request it:
- Checks the allowlist. By default there is no allowlist file and every agent joined to your relay may ask (the relay only delivers requests from joined agents). To restrict it, write
~/.config/tincan/history-allow.txtwith the agent names that may ask; then every agent in the request's chain, as the relay recorded it, must be listed. If muse asks codex and codex asks history while handling muse's request, a file that lists codex but not muse declines it because of muse. A*entry in the file means every joined agent. The file is reread for every request; an unreadable file or a bad name declines everyone. - Runs a tool-less query step: one
codex execcall that sees only the question text and turns it into a structured query (source, mode, search terms, conversation id, count,want_images, andwith_images, which picks the most recent turn that had images rather than the most recent turn). It runs read-only, with no MCP servers, no tools and no session file, and its output is checked against a schema in Go. - Reads the source. By default lookups cover the 50 most recent conversations per source, up to 30 days old. Only the owner can change that window, with
~/.config/tincan/history-window.json({"days": N, "max": N}, reread for every request); a file that is present but not valid fails every request until it is fixed. - Fills in a fixed reply template in Go and attaches up to 8 images. Retrieved chat content is never sent to a model, so text inside the owner's chats cannot steer the service.
The same readers are on the CLI: tincan history <chatgpt|claude-ai|codex|claude-code> with --latest, --list N, --search, --id, --all, --json, --images-dir, and --days N and --max N for the window.
One-time setup
Install the Tincan Chrome extension (see below), make sure codex is installed and logged in, then:
tincan history install --extension-dir ~/tincan-extension # the folder you loaded unpacked
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agenttincan.history.plist # macOS
systemctl --user daemon-reload && systemctl --user enable --now tincan-history.service # Linux
tincan history install writes the Chrome native messaging host manifest and the service definition (a launchd agent on macOS logging to ~/Library/Logs/tincan-history.log, or ~/.config/systemd/user/tincan-history.service on Linux) and prints the start command. It starts nothing. Pass --extension-dir <the folder you loaded> (or run it from a repo checkout, where it finds extension/) so the native host can reload the unpacked extension when its files change. --no-service skips the service definition.
The service does not get your login shell's PATH. Its PATH starts with the directories where codex and claude were found when you ran tincan history install, then ~/.local/bin, ~/.npm-global/bin and ~/bin, then the system directories. If you install or move codex later, run tincan history install again.
On a headless Linux box, run loginctl enable-linger $USER once so the user service keeps running after you log out.
Limits and gotchas
- It is the most sensitive agent on the mesh: by default every joined agent can read the owner's chat history. Write
~/.config/tincan/history-allow.txtto narrow that to the agents you trust with it. The allowlist governs requests to the history agent, not local shell access; an agent with a shell on the owner's machine (such as the Codex wake) can read local Codex and Claude Code history directly. - Live sources need Chrome running, the extension connected, and the owner logged in; otherwise the reply says the source is unavailable and local sources still work. Chrome is never quit or restarted.
- A query the step cannot place gets "Please ask a clearer question naming ChatGPT, claude.ai, Codex or Claude Code".
- When the window cut an answer short, the reply adds one line saying so (on the CLI, a note on stderr). ChatGPT and claude.ai read at most 100 conversations whatever the window says.
Adapter doc
The ChatGPT and Claude web agents (chatgpt-web and claude-web)
What it is
A Go service, tincan web serve --site chatgpt (or --site claude-ai), that makes chatgpt.com or claude.ai a teammate. tincan ask chatgpt-web "..." comes back with ChatGPT's answer and any images it generated attached. It types into the owner's logged-in account, as the owner, so the chats show in the owner's ChatGPT or Claude history, count against the owner's plan, and follow the site's own memory, custom instructions and model choice.
How it joins
tincan invite chatgpt-web --kind chatgpt-web # relay machine (or an admin device)
TINCAN_CONFIG=~/.config/tincan/chatgpt-web.json tincan join <code> --relay http://tincan-relay
For Claude use claude-web, --kind claude-web and ~/.config/tincan/claude-web.json. Like history, it refuses to start unless the relay confirms its name.
How it wakes
Wait: the service long-polls. Set its method to wait in wake.json.
How it sends and receives
For each request, one at a time:
- Checks the allowlist exactly like history: with no file, every joined agent may ask;
~/.config/tincan/chatgpt-web-allow.txtorclaude-web-allow.txtrestricts it to the listed names, and every agent in the chain must be listed. - Reads the optional threading line. A first line
new chatstarts a new conversation;conversation: <id>(or a conversation URL) continues that one; otherwise it continues the conversation this asker used last with this agent. Each asker has its own thread. The ids live in~/.config/tincan/<agent>-state.json(0600, ids only). Every reply ends with the conversation id so the asker can come back. - Has the extension type the message into a background tab the extension opens itself (
active: false). The extension fills the message box, clicks send, and returns once the conversation id is in the tab's address (at most 60 seconds). It never touches a tab the owner opened. - Decides completion from the conversation data, not the page: it reads the conversation through the same detail operation the history agent uses (first 5 seconds after the send, then 5, 8 and 12 seconds apart, then every 20 seconds), finds this request's own user message, and waits for the answer after it (ChatGPT: any message in the turn marked end of turn, which covers image turns whose last message is hidden; claude.ai: a
stop_reason, or the same text on 4 reads spanning at least 10 seconds). The wait is bounded by the 8 minute request timeout. An HTTP 429 waits the site'sRetry-Afteror backs off from 30 seconds up to 5 minutes, and a cooldown makes the next requests fail at once with "ChatGPT is rate-limiting this account right now; try again later" instead of hitting the site again. A rate limit that ends the wait after the send says the message was sent, names the conversation, and asks for the reply later instead of sending again. While it waits, the agent keeps its relay presence fresh without claiming new requests. - Replies with the answer text (up to 64 KB) and the generated images (up to 8) as attachments, then has the extension close the tab. A tab nobody closes is closed after 10 minutes.
Send journal: right after a send is confirmed, the agent records the request id, conversation id and send time in ~/.config/tincan/<agent>-journal.json (0600, no message text). If the relay requeues the request after its 30 minute claim lease, the agent reads the answer from the journaled conversation instead of sending again. Entries are dropped after 90 minutes.
The web agents send the request text only; messages over 32 KB are refused, not cut.
One-time setup
tincan history install --no-service # skip if history is already installed
tincan web install --site chatgpt
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.agenttincan.web.chatgpt.plist
For Claude: tincan web install --site claude-ai and com.agenttincan.web.claude-ai.plist. tincan web install writes the launchd agent (or a systemd user unit on Linux) and prints the start command; it starts nothing. Logs go to ~/Library/Logs/tincan-chatgpt-web.log (or tincan-claude-web.log). On a headless Linux box, run loginctl enable-linger $USER once so the user service keeps running after you log out.
Limits and gotchas
- It acts as the owner, and by default any joined agent may ask it to. Write its allowlist file to restrict that. The answer can include what the site remembers about the owner, and it is untrusted model output: the web agents pass it back as is.
- The extension checks the site session before opening a tab, so a logged-out browser never sends anonymously.
- Both sites protect their send endpoints with anti-bot tokens only the real page can produce, which is why it drives a tab instead of calling an API. If a site changes its page, the selectors in
extension/send.jsneed an update. - The send operations need extension version 0.3.0 or later.
Adapter doc
The Tincan Chrome extension
A Manifest V3 extension (in extension/, named "Agent Tincan History" in its manifest) that lets the history agent read, and the web agents send to, ChatGPT and claude.ai through the owner's own logged-in Chrome.
What it can do:
- Run a fixed set of operations for its native host (
tincan history native-host): list, detail and file reads for chatgpt.com and claude.ai,chatgpt.sendandclaudeai.send,chatgpt.closeandclaudeai.close, andextension.reload. Images come back as base64 in chunks of at most 384 KiB. - Open, fill and close its own background tabs for sends.
What it cannot do:
- It accepts nothing outside that operation set and never runs code from a message or a page. A message is passed as data to a fixed function in an isolated content script and inserted as text.
- No cookie or token leaves the browser. The ChatGPT access token is read inside the extension's worker and stays there.
- It never scripts a tab the owner opened, and Chrome is never quit or restarted.
- Its permissions are limited to
nativeMessaging,alarmsandscripting, on chatgpt.com,*.oaiusercontent.comand claude.ai.
Install: once the Chrome Web Store listing is published, installing is one click through Chrome's standard permission dialog. Until the store listing is live, load it unpacked once:
- Download
tincan-history-extension.zipfrom the release page and unzip it into a folder you will keep, for example~/tincan-extension(Chrome loads it from there every time, so do not delete it). From a repo checkout, theextension/folder works the same way. - Open
chrome://extensions, turn on Developer mode (top right), click Load unpacked, and pick that folder. - Run
tincan history install --extension-dir ~/tincan-extensionso Chrome can start the native host and the host can reload the extension when its files change.
The manifest carries a public key, so an unpacked load always gets the id ciejooalclcpgpapboofdbbddphldhnh. The native host accepts both that id and the Chrome Web Store build's (goldflchpojcjmifnljlfkgoahjgeajn), and when both builds are installed the store build wins automatically, so moving to the store version is just adding it from the store (then removing the unpacked copy whenever you like). Older installs pick up the store id the next time tincan history serve starts.
Self-reload: after the first load, updates need no Reload click. When the extension connects, it sends the native host its version and the sha256 of each file (hashed when its worker started). If tincan history install was run with --extension-dir (or from a repo checkout) and the files on disk differ, the host sends extension.reload and the extension calls chrome.runtime.reload(). The host checks again every 10 minutes while the extension stays connected. It waits while a send has a tab open, checking every 5 seconds for up to 5 minutes. The host asks at most once per 10 minutes for the same files when the extension connects, and the re-check never asks again for files it already asked about. A store install is never reloaded this way.
Why an extension is required: ChatGPT and claude.ai offer no official API for reading your own chat history, so the reads go through the sites' own endpoints with your existing browser session, and sends need the real page. An extension is the one way to do that inside your logged-in Chrome without exporting cookies or tokens. Chrome requires a person to click to install any extension, so that click is the one human step in the setup.
Onboarding
Once agents are joined, tincan onboard reads the live roster and writes the setup kit: a standing prompt for the Agent Tincan operator role, and for every agent its join line, the text to paste into its standing instructions, and its setup steps. It also prints add-agent recipes for every kind, a second-agent-on-one-machine recipe, and a relay-hosting recipe.
tincan onboard --operator grokbot
--operator <agent>names the always-on agent that runs the operator prompt.--section operator|agents|recipesprints one part;--jsonprints the kit as structured data (the shape theonboardMCP tool returns).--offlineskips the roster and makes no network call, so you can print the operator prompt and recipes before anyone has joined.--ownernames the person the prompts refer to;--kind name=kindtailors one agent's block.- It is read-only: it never mints invite codes or joins or removes agents. Its output never contains wake secrets. Re-run it after any roster or wake change and paste the fresh text over the old.
Kinds: vm-webhook, e2b-email, proxy-sandbox, claude-code, chatgpt, hermes, openclaw, codex, history, chatgpt-web, claude-web, generic. History and the web agents are services, so their blocks carry setup only, no standing instructions. The generic shape of an agent's instructions is in docs/adapters/agent-instructions.md.
The operator prompt follows a quiet rule: the operator speaks only when the owner asks it something or when it is answering an agent. Its 30 minute standing check never messages the owner; findings wait until the owner asks.
Several agents per machine: each agent on one machine gets its own invite and its own client config through TINCAN_CONFIG, for example Claude Code and Codex on the same Mac, or Hermes and OpenClaw on the same mini. Set that TINCAN_CONFIG on every tincan command the agent runs (join, MCP entry, listen, rejoin). tincan join refuses to overwrite a config that already names a different agent unless you pass --replace.
Self-healing rejoin: if an agent's machine is rebuilt with the same machine name (or that name with a -1 style suffix), run tincan rejoin --relay http://tincan-relay on the new machine. The relay re-admits it as the old agent, with its queued requests still waiting, when the new node is untagged, owned by the same Tailscale login, and the old node is offline or gone. Add --proxy for a proxy sandbox and --name <agent> when the machine ran several agents. Every standing instruction tells the agent to run rejoin itself and never ask for an invite unless rejoin says the machine was never joined. Tagged machines need a new invite, and tincan relay --no-auto-rebind turns this off. Each re-admission is audited as a rebind event.
Trust model
Joined agents trust each other fully: a request from a joined agent is acted on as if you asked, with no per-request approval. Tailscale is the security boundary, the relay can read every request and reply, and only admin devices can invite, remove or connect agents. The real risk is an agent that reads untrusted content being tricked into asking a powerful teammate to do something harmful. Give high-power agents instructions about what to confirm with you, keep tincan trace handy, and use tincan remove to cut an agent off.
Read docs/trust-model.md before joining an agent that reads untrusted content alongside one that holds powers like spending money. It also covers attachments, the history agent, and the web agents.
Build, test, release
make build # static ./tincan, CGO_ENABLED=0
make test # go test -race ./...
make vet # go vet ./...
make extension-test # node --test for the extension worker code (no dependencies)
make extension # dist/tincan-history-extension.zip
make dist # every release asset in dist/ (see below)
make build builds one binary, for the machine you run it on. CI runs go vet and go test -race on Linux and macOS, and checks the static builds for linux/amd64, linux/arm64, darwin/arm64 and darwin/amd64.
Releases are on the GitHub repo's release page. Each carries tincan_darwin_arm64, tincan_darwin_amd64 (Intel Mac), tincan_linux_amd64, tincan_linux_arm64, checksums.txt (sha256 of the four binaries) and tincan-history-extension.zip. The binaries and checksums.txt are what the relay's --dist directory takes (add a VERSION file).
Releases are cut by hand; CI does not publish them. From a clean checkout of the commit to release:
git tag v0.5.0 && git push origin v0.5.0
make release-mac # make dist (four static binaries, checksums.txt, extension zip in dist/), then sign and notarize the macOS binaries
gh release create v0.5.0 --title v0.5.0 dist/tincan_* dist/checksums.txt dist/tincan-history-extension.zip
make dist stamps the version from git describe, so tag first. It needs no certificate; make release-mac adds the macOS signing on a Mac that has one:
make sign-macsigns eachdist/tincan_darwin_*withcodesign --force --options runtime --timestampusingTINCAN_SIGN_IDENTITY(default: the maintainer's Developer ID Application identity), then rewriteschecksums.txt, since signing changes the bytes.make notarize-maczips each signed binary, submits it withxcrun notarytool submit --keychain-profile "$TINCAN_NOTARY_PROFILE" --wait(default profileagentcookie-notary), requiresAccepted, and checksspctl -a -vv -t installreportsNotarized Developer ID. A bare Mach-O binary cannot be stapled; Gatekeeper looks the ticket up online at first launch.- One-time notary setup:
xcrun notarytool store-credentials <profile> --apple-id <apple-id> --team-id <team-id>with an app-specific password. The credentials live in the login keychain, never in the repo.
Always upload the checksums.txt written after signing; make release-mac rewrites it last and checks it.
Quick start: docs/quickstart.md. Protocol: docs/protocol.md.
MIT licensed.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
tincan
command
Command tincan lets AI agents on one tailnet ask each other to do things.
|
Command tincan lets AI agents on one tailnet ask each other to do things. |
|
internal
|
|
|
cli
Package cli holds the tincan commands.
|
Package cli holds the tincan commands. |
|
client
Package client holds the HTTP timeout, body-size, and proxy policy shared by the relay's listeners and every agent-side client.
|
Package client holds the HTTP timeout, body-size, and proxy policy shared by the relay's listeners and every agent-side client. |
|
envelope
Package envelope defines the request and reply shapes the relay stores and delivers.
|
Package envelope defines the request and reply shapes the relay stores and delivers. |
|
gateway
Package gateway lets ChatGPT, which runs in OpenAI's cloud and cannot reach the tailnet, join as an agent.
|
Package gateway lets ChatGPT, which runs in OpenAI's cloud and cannot reach the tailnet, join as an agent. |
|
history
Package history reads Matt's conversations with ChatGPT, claude.ai, Codex and Claude Code so the history agent can answer "what did Matt last ask" and send back the images from that turn.
|
Package history reads Matt's conversations with ChatGPT, claude.ai, Codex and Claude Code so the history agent can answer "what did Matt last ask" and send back the images from that turn. |
|
identity
Package identity decides which agent sent a request.
|
Package identity decides which agent sent a request. |
|
identity/identitytest
Package identitytest provides a fake WhoIs resolver for tests.
|
Package identitytest provides a fake WhoIs resolver for tests. |
|
mcpserver
Package mcpserver exposes Agent Tincan to any MCP-capable agent (Grok Bot, Claude Code, Codex, ChatGPT through the gateway).
|
Package mcpserver exposes Agent Tincan to any MCP-capable agent (Grok Bot, Claude Code, Codex, ChatGPT through the gateway). |
|
onboard
Package onboard builds the Agent Tincan onboarding kit: a standing prompt for the operator role, a block for every agent on the roster, and recipes for adding agents and hosting the relay.
|
Package onboard builds the Agent Tincan onboarding kit: a standing prompt for the operator role, a block for every agent on the roster, and recipes for adding agents and hosting the relay. |
|
policy
Package policy fills in a request's chain and stops runaway loops between trusted agents.
|
Package policy fills in a request's chain and stops runaway loops between trusted agents. |
|
relay
Package relay is the Agent Tincan relay: an HTTP API on the tailnet that queues requests between agents and delivers them over long-poll.
|
Package relay is the Agent Tincan relay: an HTTP API on the tailnet that queues requests between agents and delivers them over long-poll. |
|
store
Package store persists the relay's state in SQLite: the agent directory, invite codes, and the request queue.
|
Package store persists the relay's state in SQLite: the agent directory, invite codes, and the request queue. |
|
testrelay
Package testrelay runs a real relay behind one httptest server per agent, each of which pins the caller's tailnet address, so client-side tests can exercise the full HTTP path without a tailnet.
|
Package testrelay runs a real relay behind one httptest server per agent, each of which pins the caller's tailnet address, so client-side tests can exercise the full HTTP path without a tailnet. |
|
wake
Package wake nudges agents that are not listening when a request arrives.
|
Package wake nudges agents that are not listening when a request arrives. |
|
spike
|
|
|
poller
command
Command spike-poller runs on each agent for U1.
|
Command spike-poller runs on each agent for U1. |
|
relay
command
Command spike-relay is the throwaway U1 listener.
|
Command spike-relay is the throwaway U1 listener. |