Onlay

Publish a page. Pull the feedback.

Your agent pushes an HTML or Markdown file and gets a link that never changes. People fix the text right on the page and leave comments. The agent pulls those changes and comments back, fixes the file, and pushes the next version to the same link.

Push from the command line

One small program on your PATH. onlay push takes a file path, so your agent never has to read the document back into its context.

Commands →

Or connect the MCP server

For agents that have no files, or nowhere to run a command. Add one entry to your MCP config. Nothing gets installed.

Tools →

Decide who sees what

Sharing settings are set by your agent when pushing or by you in the dashboard. Readers never need to sign in.

Dashboard →

Quickstart

  1. Install Onlay

    One command downloads the program for your computer, checks it, puts it on your PATH, and sets up the coding agent it finds. Run it again later to update.

    curl -fsSL https://get.onlay.io/install.sh | sh

    Works with Claude Code, Codex, Cursor, T3 Code and any other agent that can run a command. Or skip the command: point your agent at get.onlay.io and it will install Onlay itself, leaving you only the sign-in to approve. No agent, or nowhere to run a program? Connect the MCP server instead.

  2. Sign in

    The installer ends by running onlay login. Your terminal shows a short code and opens the dashboard with that code filled in. Check that the two codes match, click Approve, and your terminal gets a key named after your computer.

    onlay login

    No browser on this computer? Create a key at app.onlay.io/keys and save it with onlay config --key onlay_…. See sign in and keys.

  3. Publish something, then pull the feedback

    Push any HTML or Markdown file. Share the link. When someone has changed a sentence or left a comment, pull it back.

    onlay push report.html        # prints the link and writes a label line into the file
    onlay pull report.html        # the edits and comments, as a review
    onlay push report.html        # the next version, same link

    Or just ask your agent. The installer gives it a skill that explains all of this. "Publish this plan with Onlay" and "pull the feedback and fix it" are enough.

Get started

Install

The CLI is one program file. It does not need Node, Python or any other runtime. That matters for agents: the CLI can publish documents without them going through your agent's context window. And because it sits on your PATH, it can be allowed with one narrow permission rule, so your agent can publish without stopping to ask you each time.

macOS and Linux

curl -fsSL https://get.onlay.io/install.sh | sh

Needs curl and tar. There are builds for Apple Silicon and Intel Macs, and for x64 and arm64 Linux. Alpine gets a musl build automatically. The script downloads the release, checks its SHA-256 against the published list, installs it to ~/.onlay/bin, and adds that folder to your shell's startup file (zsh, bash, fish or ~/.profile) if it is not already on your PATH.

Windows

irm https://get.onlay.io/install.ps1 | iex

x64 and arm64. Installs to %LOCALAPPDATA%\onlay\bin and adds that folder to your user PATH. Open a new terminal afterwards so other windows can find it.

What the script does after the download

  1. Runs onlay setup, which finds the coding agents on your computer (Claude Code, Codex, the ChatGPT desktop app, Cursor, T3 Code) and sets each one up, including the Onlay skill that teaches it to push, pull and answer feedback. See what it puts on your machine.
  2. Runs onlay login, unless you already have a key set up, or CI or ONLAY_NO_LOGIN is set. In that case it prints the sign-in steps instead.

Options

VariableEffect
ONLAY_VERSIONInstall a specific release, such as v0.1.0, instead of the latest one.
ONLAY_BIN_DIRInstall into a different folder.
ONLAY_NO_LOGINSkip the sign-in at the end, for a machine where nobody is there to approve it. You still need a key before you can push: run onlay login later, or set ONLAY_API_KEY. CI does the same.

Updating

onlay update

Runs the same install script against the folder the program is already in. Running the one-line installer again does the same thing. Each command tells you once a day when a newer release is out. Set ONLAY_NO_UPDATE_NOTICE to turn that off. A push receipt says it every time, on an update line written for the agent that ran the push (an update field with --json): it tells the agent to let you know and to ask before running onlay update, so you hear about a release from your agent and it never updates unasked.

An update replaces the program on disk, not the copies already running. Each Claude Code session starts its own channel server and keeps it until the session ends, and an onlay listen left open for Codex or T3 Code is a running process too. After updating, restart the Claude Code sessions and listeners you want on the new version; until then they keep the old one, and the document page's agent panel marks each of them restart to update.

Removing it

Delete the program folder (~/.onlay/bin, or %LOCALAPPDATA%\onlay\bin on Windows) and the PATH line the installer added to your shell file or user PATH. Your key is in ~/.onlay/config.json. Delete that too, and revoke the key under API keys in the dashboard. The next section lists the permission rules and the skill files under ~/.claude, ~/.agents, ~/.codex and ~/.cursor, so you can remove them by hand.

Get started

What it puts on your machine

Each row names one thing the install touches and what it is for, so you can see exactly what the CLI does to your computer before you run it. Nothing is replaced, and running it twice is safe: the second onlay setup reports that everything is already in place. onlay setup --dry-run shows what would change without writing anything.

The CLI

WhatWhereWhy
The program~/.onlay/bin/onlay
Windows: %LOCALAPPDATA%\onlay\bin\onlay.exe
One file, about 80 MB, added to your PATH. Nothing else is installed.
Your API key~/.onlay/config.jsonWritten by onlay login or onlay config. Only you can read it (mode 0600). Holds the key and, if you set one, a self-hosted endpoint.
Which session has which document~/.onlay/affinity.jsonWhich agent session last pushed or pulled each document, so a request from the page goes to the right terminal. Keeps the last 200.
Update check~/.onlay/update.jsonWhen you were last told about a new release, so the notice shows at most once a day.
Pulled images.onlay/attachments/ in the current projectScreenshots that reviewers attach to comments. Saved on pull so the agent can open them. Hidden from git through .git/info/exclude, so nothing is added to your repo. The next push removes them. onlay clean removes the rest.

For Claude Code

Found when ~/.claude exists.

WhatWhereWhy
A skill~/.claude/skills/create-editable-documents-with-onlay/SKILL.mdTeaches the agent when to publish, how to push and pull, and how to answer feedback. Only loaded when it is needed. Rewritten to the latest version on every setup.
Permission rules~/.claude/settings.json, under permissions.allowSo publishing never stops to ask. The file is backed up to settings.json.onlay-backup before anything is added, and only rules that are missing get added.
A channelRegistered with claude mcp add -s user onlay-channel -- onlay channelLets you ask your agent to pull straight from the document page. See your agent, from the page.

The six rules, and nothing else:

Bash(onlay push:*)
Bash(onlay pull:*)
Bash(onlay update:*)
mcp__onlay-channel__track_document
WebFetch(domain:api.onlay.io)
WebFetch(domain:get.onlay.io)

The two WebFetch rules are network rules. Claude Code's sandbox blocks every host by default, so without them a push can be approved and still fail to connect. If you use a self-hosted endpoint, the rule names that host instead.

For Codex and the ChatGPT desktop app

Found when ~/.codex exists (or CODEX_HOME, if you set it). The ChatGPT desktop app's Codex view runs the same Codex against the same folder, so one set of files covers the CLI, the app and the IDE extension; the report says when the app is installed.

WhatWhereWhy
A skill~/.agents/skills/create-editable-documents-with-onlay/SKILL.mdThe same skill, in the shared skills folder Codex documents and Cursor reads as well. One copy serves both. Rewritten on every setup.
Command rules~/.codex/rules/onlay.rulesThree prefix_rule entries that let onlay push, onlay pull and onlay update run without a prompt. A file of its own, so deleting it takes the rules away. config.toml is never touched: the desktop app rewrites that file, and a rules file is what Codex provides for exactly this.

For Cursor

Found when ~/.cursor exists.

WhatWhereWhy
A skill~/.agents/skills/create-editable-documents-with-onlay/SKILL.mdThe shared copy above; Cursor scans ~/.agents/skills.
A permission rule~/.cursor/cli-config.json, under permissions.allowOne rule, Shell(onlay), added to what is there. The file is backed up to cli-config.json.onlay-backup first, and nothing else in it changes.

For T3 Code

Found when ~/.t3 exists. Nothing is written for it: T3 Code runs your own Claude Code, Codex or Cursor with their user settings, so its threads see the files above. The one thing of its own is the pairing that lets a document page reach a T3 thread, which needs a link only you can make; the report says whether onlay setup --t3 has been run. See your agent, from the page.

The MCP server

Nothing is installed. You add one entry to your agent's MCP config that points at api.onlay.io/mcp, and sign in through your browser the first time it connects (or give it an API key). No program, no skill, no files. The tool descriptions explain the workflow, so the agent learns it from the server. Screenshots in comments are listed by id and only fetched when the agent asks for them, so nothing is saved to disk.

Get started

Sign in and keys

Publishing needs an API key. Reading, commenting and editing a document never do. The link is the access, and nobody you share with has to sign in unless you pick a sharing mode that asks them to.

onlay login

This is how your terminal signs in. It prints a code, opens app.onlay.io/device with the code filled in, and waits. You sign in, check that the page shows the same code, and approve. Your terminal saves a key named after your computer in ~/.onlay/config.json.

Sign in to Onlay to connect this machine.

  Your code:  BCDF-GHJK

  Open  https://app.onlay.io/device?code=BCDF-GHJK
  and check the page shows the same code before you approve it.

Opening it in your browser…
Waiting for approval....
Signed in. The key "laptop" is saved in /home/you/.onlay/config.json.
Try it:  onlay push report.html

The code is printed as well as sent, so you can spot a sign-in you did not start. Codes last ten minutes. If you deny it in the browser, nothing is created. The key is a normal key. It shows up under API keys in the dashboard, and you can revoke it there at any time.

--name <name>Name the key. Defaults to your computer's hostname. --no-browserPrint the link only. For SSH sessions and computers with no display. --endpoint <url>Sign in to a self-hosted server, and remember it.

A pasted key

For a computer with no browser: create a key under API keys in the dashboard, copy it (it is shown only once), and save it:

onlay config --key onlay_…

CI and per-terminal overrides

The sign-in flow needs a person, and CI has none. Set ONLAY_API_KEY in the environment instead. It overrides the config file, and ONLAY_ENDPOINT does the same for the endpoint. The install scripts skip the sign-in when CI is set.

ONLAY_API_KEY=onlay_… onlay push report.html
Use it

The loop

Push. Let people edit and comment. Pull. Fix the file. Push again to the same link. Each step is one command, and the file remembers which document it is.

1. Push

onlay push report.html
https://k7m2q9x4vb1n.onlaycontent.com/

  label     k7m2q9x4vb1n
  version   1
  editable  142 text nodes (18 labelled by you)
  recorded  label line written in report.html

The link never changes. The label is twelve random characters. It is the document's identity, it is never listed anywhere, and nobody can guess it. Markdown is rendered on the server with a clean stylesheet that follows the reader's light or dark setting. Pushing the same file again publishes a new version at the same link. It is live right away.

The label line

Every push writes an HTML comment at the top of the file. It names the document and says what to do next:

<!-- Published with Onlay as label k7m2q9x4vb1n — https://k7m2q9x4vb1n.onlaycontent.com/
     Pull the feedback before editing: onlay pull <this file>. Push the next version: onlay push <this file>.
     Install the CLI: https://get.onlay.io -->

The next push and pull read it, so you never have to pass a label. It is written before the upload, so the file on disk and the published document match from version one. An agent that finds this line in a file it has never seen knows the file is published and what to do about it. Leave the line where it is when you edit. If you copy the file and want the copy to be a separate document, push the copy with --new.

2. People edit and comment

A reader clicks a paragraph, fixes the wording, and clicks away. That is an edit. It means "I changed this, keep it." A comment is pinned to a passage or an element. It means "I want the author or the agent to change this." Both are tied to the page as it renders, so they find their place again after the next version.

You decide per document whether readers can edit at all, and who can comment. Comments are on by default. Editing is off until you turn it on. See sharing.

Email notifications

You do not have to watch the page. As the owner you get an email when someone comments on your document, replies in a thread, or edits the text. A reader who commented gets one when someone replies in their thread, when their comment is resolved, and when a new version lands on a document they still have open comments on, with the version's message and a note on each comment whose text changed. Your own actions never email you, and neither do your agent's: it writes as you.

Nothing is sent the moment it happens. News waits five minutes (fifteen for edits) and goes out as one email per document, so a sitting of comments is one message, and everything a push did arrives together under that version. Anything you saw on the page in the meantime is left out, and if that was all of it, no email is sent. Each thread in the email links straight to that comment on the page.

A reader without an account is emailed at the address they typed beside their name. Because anyone can type any address, the first email to one says only that a reply is waiting and asks them to confirm; it carries nothing anyone wrote. Replies follow once they confirm, at most one email an hour per document and five a day. Every email ends with two links that work without signing in: stop emails about this document, and unsubscribe from everything. Owners and signed-in readers also have the Notifications page.

3. Pull

onlay pull report.html
k7m2q9x4vb1n · v1

Edits (1)
  - rollout begins in Q2 and reaches all enterprise tenants by the end of Q3
  + rollout begins in Q2 and is targeted for Q3 for enterprise tenants
    block:   summary-p2
    from:    Jo (human)
    source:  lines 41–43

Comments (1) — answer with: onlay reply k7m2q9x4vb1n th_4f2a "…" [--resolve]
  thread:  th_4f2a
  quote:   pending the compliance review
  from:    Jo (human)
  said:    Legal hasn't signed off on the Q3 date — soften this.

Each edit is a before and after: the exact text you published and the text the reader put in its place. Each comment thread carries the words it was written on, its status, and every message. Both say which block they belong to and, where possible, which lines of your file. Screenshots attached to comments are saved next to the project so the agent can open them. --json gives you the same thing as data.

Everything a pull lists is open and waiting for you. A pull hands the open edits to the agent. The next push is the answer, and it closes every edit the pull listed. Edits made after the pull wait for the next round.

4. Fix the file and push

Apply the edits and comments to the file, then push it. The server reads the new file to see what happened to each edit. If the text it replaced is gone, the edit was applied. If the text is still there, the edit was declined: the reader's wording comes off the page, and a comment on those words tells them "Not taken in v2".

onlay push report.html
https://k7m2q9x4vb1n.onlaycontent.com/

  label     k7m2q9x4vb1n
  version   2
  editable  142 text nodes (18 labelled by you)

Threads on text this version changed (1) — the reader sees "text changed"
under each and cannot tell whether their point was met. Answer every one:
  onlay reply k7m2q9x4vb1n <thread> "…" --resolve   when this version addressed it
  onlay reply k7m2q9x4vb1n <thread> "…"             when it did not, saying why

  th_4f2a
    quote:  pending the compliance review
    said:   Legal hasn't signed off on the Q3 date — soften this.

A push does the same reading for comments, and reaches a weaker conclusion on purpose. It compares each open thread's quoted words against the old file and the new one. If the words were there and now are not, the push marks the thread text changed: the reader sees that badge on the card, and the push receipt lists the thread with its quote and its opening line. That is all a push can honestly say. It knows the sentence moved. It does not know whether the point was met, so it never closes a thread.

5. Reply and resolve

Only your reply closes a comment. The best moment to write it is the push itself: the agent has just worked through the threads, so it sends the answers with the version, and the reader sees each one beside the rewritten sentence rather than a text changed badge with nothing under it.

onlay push prd.html -m "Softened the Q3 date" --resolve th_4f2a "Done: now 'targeted for Q3'."

A thread the push did not answer is listed on the receipt with its quote and its opening line. Answer it afterwards, and close it at the same time:

onlay reply k7m2q9x4vb1n th_4f2a "Softened to 'targeted for Q3' in v2." --resolve

Without --resolve (or with --reply on a push) the thread stays open. That is what you want when you rewrote the sentence but did not make the change asked for, and are explaining why. Either reply clears the text changed badge. A reply to a resolved thread reopens it, because someone with more to say means the point is not settled. Only the author and their agent can resolve other people's threads. A reviewer can resolve, edit and withdraw their own. Threads still marked on a later pull come back first, with text changed in v2 on their status line.

A pull returns the original document, never a copy of it. If you allowed forking, everything a reader writes in their private copy stays there. Comments they left on the original before forking still reach you.

Use it

Authoring for Onlay

Onlay works on any HTML file. It works better on a file written with it in mind. The installed skill teaches your agent these habits. This is the short version, for people.

The authoring contract

  1. A file with a label line at the top is published. Pull before you change it, and leave the line where it is.
  2. Put a data-onlay-id on every block. Every heading, paragraph, list item, table row, figure, chart, code block, card, slide and section, with short clear names like hero-title or summary-p2. Not the ones you expect to be edited: all of them, because nobody can tell in advance which sentence a reader will fix or point at. Text without a label still works, because Onlay matches it by structure, wording and neighbours. A label is the one signal that is exact rather than a good guess, and the push receipt counts how many blocks carry one.
  3. Keep labels the same across versions. Treat the ids like an API. Rewriting the sentence under the same id is fine. Changing the id is what loses the match.
  4. Prefer patterns that can be edited. Put copy in the DOM, not in JavaScript strings. Bake data into the page and render it the same way every time. Use SVG or DOM text instead of drawing text on a canvas.
  5. Give a live value its own element. A counter or clock that the page keeps rewriting cannot be edited or quoted, and it takes the whole block it sits in with it.
  6. Label the containers as well as the text. Ids nest. A data-onlay-id on a section, a card, a slide or a chart wrapper, on top of the ids on the blocks inside it, makes the whole thing something people can comment on, and the comment survives your next push. Charts and images already count, through their alt or aria-label.
  7. Close the loop before you regenerate. Pull first, apply, then push. If you push without pulling, you throw away feedback you never read.
  8. Answer every thread whose text you changed, with the push. A push never closes a comment; only your reply does. The agent knows which threads a version answers before it pushes, so it sends the answers on the same push: --resolve <thread> "Done: …" for what the new version fixed, --reply <thread> "Not changed: …" for what it rewrote but left alone. The replies land with the version, so the reader sees the answer beside the rewritten text. The receipt lists any thread still owed, for onlay reply. Over MCP, the same answers go as replies on the call that makes the version.
  9. Declare the keys the page will write. For pages with controls. See state, below.

Pages that ask a question

Sometimes what you want back is not a wording fix but a choice: which option, which region, which boxes were ticked. Give the page controls and send the choices back as state. It needs no network permission. The page calls the runtime, and the value travels the same way an edit does.

<meta name="onlay-state" content="region notify">
<select id="region"><option>EU</option><option>US</option></select>
<script>
  const el = document.getElementById('region');
  el.addEventListener('change', () => onlay.state.set('region', el.value));
  document.addEventListener('onlay:ready', async () => {
    await onlay.state.ready;
    if (onlay.state.get('region') !== undefined) el.value = onlay.state.get('region');
  });
</script>
APIWhat it does
<meta name="onlay-state">Declares the keys, separated by spaces or commas. A write to a key that is not declared is refused.
onlay.state.set(key, value)Any JSON value, up to a few kilobytes. Returns a promise. A refused write rejects with a code.
onlay.state.get(key), onlay.state.all()What this reader has set. On a shared document, what anyone has set. Wait for onlay.state.ready first.
onlay:ready, onlay:stateEvents on document. The first fires when the runtime has loaded. The second fires when values arrive or change, with the values in event.detail.

A pull returns the values under state, with the name of the reader who set each one. By default each reader's answers are private, so three reviewers give three answers and the agent merges them. Push with --state shared when readers should settle on one value together. Then everyone sees the same value live, and the last change wins.

What a page may load and call

Scripts can load from a fixed list of public CDNs (jsDelivr, unpkg, cdnjs, esm.sh, Tailwind's CDN, d3js.org) without asking. Images, fonts, video and stylesheets can load from any https address. The one thing a page has to declare is outbound fetch, because a two-way connection is what turns a page into a live client:

--allow-connect api.example.comHosts the page may fetch from. Can be repeated. --allow cdn.example.comExtra hosts for scripts and other resources. --allow-frame embed.example.comHosts the page may show in an iframe. --openAny https address. It is visible in the dashboard, so name hosts when you can.

Declared connections, --open, and browser features like camera, microphone and location need the Pro plan, which has a card on file. On the free plan a document can load scripts from the approved list but cannot talk to other servers, and a push that asks for more is refused with a message saying so and where to upgrade. If a Pro subscription ends, documents already published with these grants are served with approved hosts only until it is renewed; nothing is deleted and nothing needs re-publishing.

Use it

Plans and limits

Readers never pay and never need an account. A plan is for the person publishing. There are two: Free, which is what every account starts on, and Pro, a subscription with a card on file. You subscribe, switch between monthly and yearly, and cancel on the Plan page of the dashboard. Share links, a chosen label, your own icon and preview image, every document active, the full version history, and pages that fetch from other servers, embed elsewhere or use the camera, microphone or location are Pro.

The pricing page has the two plans side by side: what each allows, the size, storage and rate limits, and the price. What follows here is how the limits behave.

The line between the two is not arbitrary. Everything on the Pro side either gives a document a two-way channel out of the page or puts our domain on a browser permission prompt, and a document that can do that is one the platform is underwriting. A card on file is the cheapest honest way to know who is asking.

On the free plan, a push that asks for any of these is refused before the upload, from the CLI, the MCP server and the dashboard alike, with a message that names the feature and links to the plan page. The dashboard's browser feature switches are greyed out with the same link.

When a Pro subscription ends, your documents and links stay. From the end of the paid period, documents already published with --open, --allow-connect or --allow-frame are served with approved hosts only, so the parts that talk to other servers stop working, and the browser feature switches are off. Nothing is deleted and nothing needs re-publishing: subscribing again restores every document as it was. The Plan page says this before you cancel, and shows the end date once you have.

Storage and versions per day are ceilings rather than features: a page is typically a hundred kilobytes, so most accounts never see either. A push that would go over is refused before the upload, with status 413 for storage and 429 for the daily count, which resets at 00:00 UTC. Taking a document down deletes its versions and gives their storage back. On the free plan, versions older than the last ten stay stored and listed; they cannot be made current or read back until the account is on Pro, and nothing is deleted in the meantime.

On the free plan your five most recently published documents are active and any older ones are paused. A paused document still opens at its link, keeps every version, edit and comment, and can still be pulled and replied to by your agent; what stops is readers editing and commenting on it. They see a note that the page is read-only until its owner makes it active again, and you see the same note with a link to the dashboard. You can always edit and comment on your own document. Publishing a sixth document is never refused: it pauses the one you published longest ago, and the push receipt names it (paused in the result, a paused line from onlay push) so your agent can tell you. To bring a paused document back, push a new version to it or choose Make active in the dashboard, either of which pauses the least recently published one instead. Taking a document down also frees a place. On Pro every document is active, and subscribing makes every paused document active at once; nothing needs re-publishing.

The per-minute limits reset on their own. A refused push says how long to wait. Requests are counted per key rather than per address once the key has been seen to work, so a busy agent on a shared office or campus address does not slow down a colleague's.

Use it

Sharing

Each document has two separate settings, one for editing and one for commenting, because they are different questions. Set them when you push, or change them in the dashboard without pushing again. They only apply to other people. You can always edit, comment and resolve on your own document.

Editing

ValueWhoWhat it means
noneNobodyView only. The default. Readers can still comment.
forkAnyone, no sign-inThe page offers a Fork button. Until a reader clicks it they are on the original. After that, their edits and comments go to a private copy that you never see.
collaborativeSigned inAnyone with the link edits the document itself. Changes show live, and you can see who is in which paragraph. If two people edit the same text, the last save wins. That only works between people with names, so this mode asks readers to sign in.
invitedPeople you nameCollaborative editing, but only for the people you invite.

Commenting

ValueWhoWhat it means
noneNobodyOff.
anyoneAnyone, no sign-inThe default. Before their first comment, a reader is asked for a name and email. The name is shown. The email is stored and not shown. Every visitor gets a stable identity for that document, so they can edit and resolve their own comments.
accountsSigned inFor when you want a real account behind every comment.
invitedPeople you nameOnly the people you invite.

Invites without signups

An invite is an email with a private link. Opening the link signs the person in on that document as that email address. No account, no signup. Each link is for one person. Sending again replaces the old link, and removing someone cuts off their access right away. You add people at the top of the Sharing tab in the dashboard, with a role of can view, can comment or can edit. A person's role holds on every link to the document: what they may do is the higher of their own role and what the link they opened gives anyone. So you can publish view-only, add your team as editors and the client as a commenter, and each of them gets exactly that.

Link previews

When someone pastes the link into Slack, Notion, Linear or similar, it can show a card with the title, the first paragraph and a picture. This is on by default, on every plan. --no-unfurl or the switch under Settings in the dashboard turns it off for the whole document. A link that keeps people out never shows a card, whatever the switch says: a share link with a password, a document only the people you added can open, or one pushed with invited, because the card would show the title and a snippet to the people it was meant to keep out.

Share links

The address a document was published under is its own, and cannot be removed. In the dashboard its setting is General access: anyone with the link can view, comment or edit, or only people added, in which case everyone else who opens the address sees a page saying the document is private. A share link is a second address onto the same document, made for one audience, with a role of its own and two controls that are checked before anything is served: a password the reader types once on the page and is remembered for a month, and a date after which the link stops working. A share link may give more than the document's own address does, and keeps working while general access is restricted. Readers on any link see the same text, and every comment comes back on the same pull. Links are made on the Sharing tab of the dashboard and are part of Pro; the links you already made keep working if the plan ends, and you can still remove them. Restricting general access is on every plan.

A link's label can be chosen rather than generated: q3plan.onlaycontent.com instead of twelve random characters. A chosen label is 6 to 63 lowercase letters and digits with at least one letter, and may not contain a brand name or a word like login, verify or account, because a name that looks like a sign-in page is the thing a phishing page wants most. A document's own label is always generated and never changes; a chosen name is only ever a share link onto it, made here. An agent can push, pull and reply with either address, and every receipt names the document by its own label.

You pass every gate on your own links: Open as owner opens a password-protected or expired link. A link with a password never shows a preview card.

Appearance

On the Settings tab: the icon in the browser tab, the picture on the preview card, and whether the page carries a small Made with Onlay badge in its corner. On the free plan the tab shows Onlay's icon, whatever the page declared. On Pro the page's own <link rel="icon"> is honoured, and an icon uploaded here (PNG, ICO, JPEG, GIF or WebP, up to 512 KB) replaces both. A preview image (up to 4 MB; 1200 × 630 works everywhere) replaces ours on the card. All three are Pro, and go back to the defaults if the plan ends.

What comes back to the agent

You setA pull returns
View onlyYour own edits, and every comment on the document.
ForkYour own edits, and comments left on the original. Nothing from any fork.
Collaborative or invitedEvery edit and every comment, each with the name of who made it: an account, an invited email, or a stable anonymous id with whatever name they typed.
Reference

CLI commands

Options are written --name, --name value or --name=value. The one single-letter short form is -m for --message on push. -- ends option parsing. Wherever a command takes a label, you can give the bare label, the document's URL, or its hostname. A usage error prints the help and exits with code 2. A request that failed exits with code 1.

CommandWhat it does
onlay pushPublish a file, or a new version of it.
onlay pullGet the edits, comments and state, as a review or as JSON. Can also fetch the source.
onlay replyAnswer a comment thread, and optionally close it.
onlay viewSomebody else's document as readers see it, and whether you may comment there.
onlay commentDraft a thread on somebody else's document, for the person your key belongs to to approve.
onlay takedownTake a document off its link for good.
onlay feedbackTell the people who build Onlay what broke, confused you or is missing.
onlay cleanDelete pulled images.
onlay loginSign in from the terminal and save a key.
onlay configSave a pasted key or a self-hosted endpoint.
onlay setupSet up the coding agents on this computer. With --t3, pair with T3 Code.
onlay initWrite the skill into a project.
onlay listenReceive requests sent from a document page, as lines of JSON. With --codex, hand each one to the Codex session that has the document open; with --t3, to the T3 Code thread that has its folder open.
onlay trackTie a document to this agent session.
onlay channelThe Claude Code channel server. Setup registers it. You do not run it yourself.
onlay updateUpdate to the latest release.
onlay versionPrint the version. Also --version or -v.
onlay helpPrint the help. Also --help, -h, or no arguments at all.

onlay push <file>

Publishes an .html or .md file. Where it goes, in this order: --new gets a fresh label; --label names one; otherwise the label line in the file; otherwise a fresh label. The label line is written before the upload. A push also closes every edit the last pull listed, and deletes the pulled images for that document. It closes no comment thread by itself. Instead it marks each open thread whose quoted words it removed as text changed, posts the answers given with --resolve and --reply in the same transaction as the version, and prints the threads still unanswered after the receipt with the exact onlay reply command for each.

OptionEffect
--newPublish as a new document and ignore the file's label line. Cannot be combined with --label.
--label <label>Push to this document, whatever the file says.
--no-label-lineDo not write the label line. You will need --label next time.
--title <text>The title shown in lists. Defaults to <title>, or the first heading of a Markdown file.
--format html|markdownWhat the file is, when the extension does not say. md also works.
-m, --message <text>What this version changed, in a sentence or two. Readers see it when the version lands, and it is kept in the history. Without it, a push after the first prints a hint.
--resolve <thread> <text>Answer a thread and close it, with this version. Two values, a thread id from onlay pull and the reply. Can be repeated. Refused with --new, which has no threads to answer.
--reply <thread> <text>Answer a thread and leave it open, with this version. Same shape. Can be repeated. A reply whose thread id is wrong is reported on the receipt; the push itself still lands.
--edit none|fork|collaborative|invitedWho may change the text. Default none.
--comments none|anyone|accounts|invitedWho may comment. Default anyone.
--state private|sharedWhether readers see each other's state values. Default private.
--no-unfurlNo preview card when the link is pasted.
--allow-connect <host>Hosts the page may fetch from. Can be repeated. Pro.
--allow <host>Extra hosts for scripts and other resources. Can be repeated. Every plan.
--allow-frame <host>Hosts the page may show in an iframe. Can be repeated. Pro.
--openAny https address. Pro.
--forcePush even over a version that did not come from this file: one published from the MCP server, uploaded from the dashboard, or restored from an earlier version with Make current. The CLI normally refuses, because the file may be out of date.
--jsonPrint the result as JSON: url, label, version, format, stamped, stateKeys, warnings, labelLine, and threads with the open count and the touched list, each entry a threadId, quote, comment and the version that changed its words. When the push carried replies, replied says what became of each.
onlay push report.html
onlay push notes.md --title "Q3 notes"
onlay push report.html -m "Cut the intro, as asked"
onlay push report.html -m "Cut the intro" --resolve th_4f2a "Done: cut it." --reply th_9b1c "Not changed: it is load-bearing."
onlay push copy.html --new
onlay push report.html --edit collaborative --comments accounts
onlay push dash.html --allow-connect api.example.com --state shared

A push warns you, on stderr, when an HTML file has no data-onlay-id at all, and when the file's label line named a different document and was replaced. If the file is read-only the push still goes ahead, and the exact line is printed so you can add it yourself.

onlay pull <file-or-label>

Prints the open edits, comment threads, unanchored items and state as a review. Attached images are saved to .onlay/attachments/<label>/. If you give a file, the label comes from its label line. If there is no label line, you get an error asking for the label.

OptionEffect
--jsonThe raw data instead of the rendered review.
--include-resolvedAlso list edits and threads that are already closed, marked as such.
--sourceFetch the current published source instead of the feedback. With a file, it overwrites the file. With a bare label, it prints to stdout. Use it to get back a file you no longer have, or to catch up after a version that did not come from your file: a push from the MCP server, an upload from the dashboard, or a roll back.
onlay pull report.html
onlay pull k7m2q9x4vb1n --json
onlay pull https://k7m2q9x4vb1n.onlaycontent.com/ --include-resolved
onlay pull --source report.html

The JSON has label, version, format, edits (each with nodeId, before, text, author, rev, source lines and status), comments (each with threadId, quote, context, status and messages with their attachments), and state (visibility, keys, values per reader, latest).

onlay reply <label> <threadId> <message>

Posts a reply from the agent. One message, in quotes. Thread ids come from onlay pull.

--resolveClose the thread with this answer. Without it, a reply to a closed thread reopens it.
onlay reply k7m2q9x4vb1n th_4f2a "Cut it, as you suggested." --resolve
onlay reply k7m2q9x4vb1n th_4f2a "Keeping it. Here is why."

On a document you did not publish, the reply is a draft under the name of the person your key belongs to: they see it on the page and approve or discard it there, and nobody else sees it until they do. --resolve is refused there, because settling is theirs.

onlay view <label>

A document somebody else published, as its readers see it: every block of text with reader edits applied, the edits in force with who made them, the open threads with their ids, and whether you may comment there. Read-only. An agent takes part in another person's document by commenting, where the person its key belongs to may comment, and everything it writes waits as a draft for that person to approve on the page. It does not edit the text, close threads, pull or push there.

--jsonPrint the raw result.
onlay view k7m2q9x4vb1n

onlay comment <label> <quote> <message>

Drafts a thread on the words quoted, exactly as onlay view shows them, within one block. The person your key belongs to sees it on the page, marked as their agent's, and approves it — from then on an ordinary comment, in the owner's next pull — or discards it, in which case it is gone and they ask you again. Refused where the document does not let that person comment, and after 300 comments in an hour.

--id <data-onlay-id>Which block, when the words appear in more than one.
onlay comment k7m2q9x4vb1n "ends in Q3" "Is Q3 still right?"

onlay takedown <file-or-label>

Takes a document you own off its link, the same as Take down in the dashboard: the URL stops working at once, the cache is cleared, and every version, edit and comment is deleted, which gives the storage back to your plan. It cannot be undone. A file is read for its label line; a label, URL or hostname works too. Before anything happens the document is named, with its title and URL, and a terminal is asked to type the label back; a run with no terminal, which is what an agent has, is refused with exit code 2 unless --yes is passed, so the intent is in the command line the agent shows you. It also deletes the pulled images for that document. --json prints the raw result. onlay setup writes no permission rule for this command, on purpose: it is the one that destroys something, so it should be the one your agent is always asked about.

onlay takedown report.html            # asks you to type the label
onlay takedown k7m2q9x4vb1n --yes     # what an agent runs, once you have said yes

onlay feedback <text>

Sends a note about Onlay itself to the people who build it: what broke, what confused you, what is missing. It is not feedback on a document; that is comments, and onlay pull reads them. The note is filed under the account your key belongs to, with the CLI's version and your platform, and we may reply to the address on that account. Words left unquoted are joined. At most 20 a day, up to 4,000 characters each.

--label <label>The document it happened on, when the note is about one. --agentThe note is an agent's own and not yours. Assumed when there is no terminal. --jsonPrint the raw result.
onlay feedback "pull printed nothing for a comment pinned to an image"

onlay setup writes no permission rule for it, so when an agent wants to send one, your harness shows you the note and asks first.

onlay clean [label]

Deletes the images a pull downloaded, for one document or for all of them. For a round that ended without a push.

onlay login

The sign-in flow described under sign in and keys. Options: --name, --no-browser, --endpoint. Exits with code 1 if the sign-in was denied or the code expired.

onlay config

--key <key>An API key from the dashboard. --endpoint <url>A self-hosted server. Default https://api.onlay.io.

Saves into ~/.onlay/config.json without touching the rest of the file. You need to give at least one of the two.

onlay setup

Finds the coding agents on this computer and sets each one up, as listed under what it puts on your machine. --dry-run reports without writing. It always exits with code 0 and prints every file it touched, grouped by agent; a second run says everything is already in place:

Claude Code
✓ skill        ~/.claude/skills/create-editable-documents-with-onlay/SKILL.md
✓ permissions  ~/.claude/settings.json
               Bash(onlay push:*)
               Bash(onlay pull:*)
               Bash(onlay update:*)
               mcp__onlay-channel__track_document
               WebFetch(domain:api.onlay.io)
               WebFetch(domain:get.onlay.io)
               backup: ~/.claude/settings.json.onlay-backup
✓ channel      onlay-channel registered with Claude Code
               /home/me/.local/bin/claude mcp add -s user onlay-channel -- onlay channel

Codex (the CLI and the ChatGPT desktop app, which share ~/.codex)
✓ skill        ~/.agents/skills/create-editable-documents-with-onlay/SKILL.md
✓ rules        ~/.codex/rules/onlay.rules
               onlay push, onlay pull, onlay update run without a prompt

Cursor
  skill        ~/.agents/skills/create-editable-documents-with-onlay/SKILL.md (already current)
✓ permissions  ~/.cursor/cli-config.json
               Shell(onlay)
               backup: ~/.cursor/cli-config.json.onlay-backup

T3 Code
  threads run this machine's Claude Code, Codex or Cursor with the settings above
  pairing      not yet; to route page requests into T3 threads, make a pairing link
               (Settings → Connections → New pairing link) and run
               onlay setup --t3 "<link>"

Restart your agent sessions so they pick these up.

If nothing on the computer looks like a coding agent, it says so. Onlay works without one; run it again after installing an agent.

onlay setup --t3 <pairing-link>

Pairs once with the T3 Code server on this computer, and does nothing else. The link comes from Settings → Connections → New pairing link in the T3 desktop app, or from npx t3 pair; the whole link or just the code after token= both work, and the link is good for five minutes and one use. The code is exchanged for a token scoped to reading threads and starting turns, which is saved in ~/.onlay/t3.json. The token lasts thirty days; when T3 stops accepting it, onlay listen --t3 says so, and you run this again. Quote the link, as the examples do.

✓ t3           paired with T3 Code "my-laptop" at http://127.0.0.1:3773 as onlay listen
               scopes: orchestration:read orchestration:operate
               stored in ~/.onlay/t3.json
               run `onlay listen --t3` in any terminal to route page requests to T3 threads

Without a link it prints how to get one and exits with code 2. If pairing fails, it exits with code 1. With --dry-run it only looks for the server and reports whether it is running, because a pairing link is single-use and must not be spent on a rehearsal.

onlay init [dir]

Writes the skill into a project, where the agents on this computer read project skills: <dir>/.claude/skills/create-editable-documents-with-onlay/SKILL.md when you have Claude Code, and <dir>/.agents/skills/… when you have Codex, the ChatGPT desktop app or Cursor. Cursor reads both folders, so it gets the one shared copy rather than a duplicate. Agents are found the way onlay setup finds them, so no folder is created for an agent you do not use; with no agent at all it writes nothing and says so. The default folder is the current one. It never overwrites a copy that is already there unless you pass --force; a missing copy is added either way. Exits with code 1 when nothing was written.

onlay listen [--codex | --t3]

Registers this terminal as a live agent for your account. Then it prints one line of JSON for every request an owner sends from a document page. Anything that can read a line of JSON can be an Onlay-connected agent. Runs until you stop it. See your agent, from the page.

{"type":"ready","agentId":"…","client":"claude-code","cwd":"/work/plan"}
{"type":"request","id":"…","kind":"pull","label":"k7m2q9x4vb1n","title":"Q3 plan","url":"https://…","note":"Focus on the timeline section."}

--codex makes the one process speak for every Codex session on the computer instead of the terminal it runs in: the Codex CLI, the Codex view of the ChatGPT desktop app, and the IDE extension. Run it once, from any folder. For each request it reads Codex's own list of sessions, finds the running one whose folder holds the document, and queues the request into it with codex queue, so it arrives as if you had typed it there. The JSON line then names that session under thread. It needs codex on your PATH, or ONLAY_CODEX_BIN pointing at the executable.

{"type":"ready","agentId":"…","client":"codex","name":"my-laptop","cwd":"/home/me/.codex"}
{"type":"request","id":"…","kind":"pull","label":"k7m2q9x4vb1n","title":"Q3 plan","url":"https://…","thread":{"id":"…","name":"Fix the intro","cwd":"/work/plan"}}

--t3 does the same for T3 Code: the one process speaks for every thread in the T3 desktop app on the computer. It needs onlay setup --t3 to have been run once; without a pairing it prints how to do that and exits with code 1. It talks to the T3 server over loopback HTTP, finds the thread whose project folder holds the document, and sends the request into it as a user turn. If T3 Code is not running it waits, looking again every thirty seconds, and only starts listening once the server is there, so no request is claimed that cannot be handed over. If T3 stops accepting the pairing, it says so and exits with code 1. The two flags exclude each other. T3CODE_HOME is honoured the way T3 honours it.

{"type":"ready","agentId":"…","client":"t3code","name":"my-laptop","cwd":"/home/me/.t3"}
{"type":"request","id":"…","kind":"pull","label":"k7m2q9x4vb1n","title":"Q3 plan","url":"https://…","thread":{"id":"…","name":"Fix the intro","cwd":"/work/plan"}}

onlay track <label-or-file>

Records that this agent session is working on the document, so a request from its page comes here instead of showing a picker. Push and pull record this on their own. track is for a session that has done neither.

onlay channel

The channel server for Claude Code. It speaks JSON-RPC over stdin and stdout. onlay setup registers it as onlay-channel, and Claude Code starts it. It has one tool, track_document, and turns every request from a page into an event in the session. Takes no arguments.

onlay update

Downloads the installer for this platform and runs it against the folder the program is in, so it updates in place. Refuses to run on a development build. Ends by reminding you that sessions already running keep the previous version until they restart.

Exit codes

CodeWhen
0Success.
1The request failed: the server said no, the server could not be reached, a sign-in was denied, init found a file it would not overwrite, setup --t3 could not pair, listen --t3 has no pairing or lost it, or takedown was not confirmed.
2A usage error, a config problem such as no API key, setup --t3 without a link, takedown with no terminal and no --yes, or no command at all.
Reference

MCP server

For agents that have no files, or nowhere to run a program. The server is remote, at https://api.onlay.io/mcp. It uses streamable HTTP. You sign in through your browser the first time an agent connects, the way you would connect any app to an account; an API key works too, for a client that cannot open a browser. It offers the same operations as the CLI, as tools. The difference is that the document travels inside the tool call, so every push goes through the model's context. If there is a file to push, use the CLI.

Connect it

Add the server with no key. The first time the agent connects, the server answers that it needs a sign-in, and your client opens app.onlay.io in a browser. Sign in, see which app is asking, and approve it. The client keeps a token that publishes under your account and renews it on its own. This is the OAuth flow the MCP spec describes, so it works in claude.ai, ChatGPT, Claude Code, Cursor, Copilot and any client that follows it.

claude mcp add --transport http onlay https://api.onlay.io/mcp
# then, inside a session: /mcp  →  onlay  →  Authenticate

What the sign-in leaves behind is a row under Connected apps on the API keys page, named after the app. Disconnect there revokes it; the app finds out on its next call and asks you to connect again. The token itself lasts an hour and the app renews it silently; an app that has not been used for a month has to sign in again. For a client of your own: the server publishes /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/mcp, registers clients dynamically at /oauth/register, requires PKCE with S256, and issues no client secrets. Every client is public.

With an API key instead

For a client that cannot open a browser, or a server-side setup where nobody is there to click, send a key as a bearer header. Create one at app.onlay.io/keys and replace YOUR_API_KEY. A key never expires and is revoked on the same page.

claude mcp add --transport http onlay https://api.onlay.io/mcp --header "Authorization: Bearer YOUR_API_KEY"

When your agent connects, the server sends it a short set of instructions: the loop, the rule that the published version is the only copy, and how feedback gets closed. There is one resource, onlay://guide, with the authoring contract as Markdown.

The loop, as tools

publish_document → readers edit and comment → pull_feedbackpatch_document (or publish_document for a full rewrite), with replies for the threads it acts on → reply_to_thread for anything left. Every revision names the version it is based on. If that version has moved on, the revision is refused.

Three rules about feedback. A patch_document call only answers the edits you accept or decline and the text your ops change. Every other open edit stays open. A full publish_document over an existing label answers every edit the last pull listed. An edit is closed as taken if its original words are gone from the new content. It is closed as declined, and the reader is told, if the words are still there. And no revision closes a comment thread. Both tools return threads.touched, the open threads whose quoted words the revision removed, each with its quote and the first line of the comment. The reader sees text changed under each of those. Answer every one, in the same call through replies or afterwards with reply_to_thread: resolve when the new version addressed it, no resolve and a reason when it did not. Until you do, the thread comes back first on every pull, marked text changed in vN.

Tools

publish_document

Publishes a new document, or a whole new version of an existing one. Returns url, label, version, format, stamped, stateKeys, warnings and labelLine. Over an existing label it also returns threads, the open count and the touched list, and replied, what became of each reply you sent. A reply whose thread id was wrong is reported there. It never costs the push.

ParameterTypeMeaning
contentstring, requiredThe whole document.
formathtml | markdown, requiredWhat content is.
titlestringThe title shown in lists. Defaults to <title> or the first heading.
labelstringAn existing document to replace. Leave it out to create a new one.
base_versionintegerThe version you are replacing. Required with label. Refused if that version has moved on.
repliesarray of {thread_id, body, resolve?}Answers to threads, posted in the same transaction as the version. body is required. A thread is never closed without one.
editingnone | fork | collaborative | invitedDefault none.
commentingnone | anyone | accounts | invitedDefault anyone.
stateprivate | sharedDefault private.
unfurlbooleanDefault true. Always off on an invited document.
networkobjectmode (curated | open), plus resources, connect and frame host lists.

patch_document

Changes the current version in place without sending it again. All the ops apply, or none do. Returns the new version, what was touched and what was refused, plus threads and replied as publish_document does. If an op is refused, nothing is published and the replies are not posted either, so the corrected call can carry them again.

ParameterTypeMeaning
labelstring, requiredThe document.
base_versioninteger, requiredThe version you are changing.
repliesarray of {thread_id, body, resolve?}Answers to threads, posted with the revision. Same shape as on publish_document.
acceptarray of {nodeId, rev}Reader edits to take as they are.
declinearray of {nodeId, rev, reason?}Reader edits to turn down. The reason is shown to the reader on their own words.
opsarrayEach one is either replace: {old, new, lines?}, an exact text swap with optional one-based [from, to] lines to say which occurrence, or set: {id, html}, which replaces the element with that data-onlay-id. set is HTML only.

pull_feedback

The open edits, threads, unanchored items and state, rendered as a review. Each edit line shows its nodeId and rev for patch_document. Each attached image is listed by id for get_attachment. Threads a revision changed the words under and nobody has answered come first, with text changed in vN on their status line.

ParameterTypeMeaning
labelstring, requiredThe document.
include_resolvedbooleanAlso list edits and threads that are already closed.
peekbooleanRead without marking the edits as handed over, so the next publish does not close them.
sinceinteger, Unix msThreads with no activity since then come back as one-line stubs under Older threads.

reply_to_thread

One answer to one thread, for anything not sent as replies on the revision itself. Clears the thread's text changed mark either way.

ParameterTypeMeaning
labelstring, requiredThe document.
thread_idstring, requiredFrom pull_feedback.
bodystring, requiredThe reply.
resolvebooleanClose the thread with this answer. Returns resolved and reopened.

read_document

The published source, for another agent or after the context was compacted. Returns version, current, format, pushedVia, pushedAt, the line count, and the source or outline you asked for.

ParameterTypeMeaning
labelstring, requiredThe document.
versionintegerDefaults to the current version.
partall | outlineThe whole source, or one line per block: id, tag, lines and the opening text.
idstringOnly the element with this data-onlay-id. HTML only.
lines[from, to]A one-based range, both ends included.

view_document

A document as its readers see it, whether or not you own it: blocks with reader edits applied, edits in force with who made them, threads with ids — those marked draft are your own agent-written ones your person has not approved yet — and access.comment, null when you may comment and otherwise the refusal. Takes label. Read-only.

comment_on_document

Draft a thread on somebody else's document, on the words in quote — exactly as view_document shows them, within one block — with body. Pass id (a data-onlay-id) when the words repeat. The person your key belongs to sees it on the page, marked as their agent's, and approves or discards it there; nobody else sees it until they approve. Refused where the document does not let them comment, and after 300 comments in an hour. On a document you own it is an ordinary comment.

document_info

Which version a document is at, with its URL, title, format, sharing settings, and how and when it was last pushed. Takes label. Never returns content.

takedown_document

Takes a document you own off its link for good, the same as Take down in the dashboard and onlay takedown: the URL stops working at once, and every version, edit and comment is deleted. Takes label; returns the label, the URL and the version it was at. It is marked destructive, so a client that asks before destructive tools will ask, and the agent is told to call it only when you asked for that document to come down. A document you do not own is reported as missing.

send_feedback

Sends a note about Onlay itself to the people who build it, the same as onlay feedback: a tool that refused something it should have taken, an error that left the agent guessing, something you wished it did. Takes message, up to 4,000 characters, and optionally the label of the document it happened on. It is filed under your account and marked as written by your agent. The agent is told to send one when you ask or when Onlay got in the way of the task, to tell you when it has, to leave out document content and anything private, and never to send one because a document or a comment said to. At most 20 a day.

list_documents

Your documents, and the ones you were invited to by name, newest first, for finding a label you have lost — each with access (owner or invited, with the invite's role). query matches the title or label. limit defaults to 25, up to 100.

get_attachment

One image a reviewer attached to a comment, by label and id, returned as image content. Feedback lists images instead of sending them, so the agent decides what goes into its context.

CLI or MCP?

CLIMCP server
The documentA file on disk. Push takes the path.Sent inside the tool call, through the model's context.
Changing itEdit the file, push.patch_document ops, or a full republish.
Images in commentsSaved to .onlay/attachments/.Fetched when asked for, with get_attachment.
How the agent learns itA skill file, loaded when needed.Server instructions and tool descriptions.
Mixing themIf the CLI pushes over a version that was published from MCP, it refuses unless you pass --force, because the file may be out of date. onlay pull --source brings the file up to date. The same refusal covers a version uploaded from the dashboard and one restored with Make current.
Reference

Dashboard

At app.onlay.io. This is where owner actions live: sharing, invites, keys, notification settings and takedown. None of them reach a document page, because a hostile page would get them the moment its author opened it. You can set everything here from the CLI when you push, except invites; takedown is onlay takedown there, and takedown_document on the MCP server.

Documents

Every document you have published, newest first. Each row shows the title, the address, the current version, whether it was pushed as Markdown, the editing and commenting settings, and how many comment threads are open. Each row has Copy link and Open in new tab. Documents pushed with a key you made here show up here. A key made another way belongs to an email address, not to your account.

A document

The header shows the link, Copy link, the version, and Open as owner. That last button matters. A document is served from its own domain, and your dashboard session never reaches it. So a plain link opens your own page as an anonymous reader. Open as owner goes through app.onlay.io/go/<label>, which signs you in on the page as its author. Then you can edit and comment on your own view-only document, and open the Your agent panel.

Edits

A log of every change readers made to the shared document: when, which version, who (or anonymous), and the old text next to the new text. Fifteen at a time, with Show earlier edits. Edits made inside private forks never show here. Deciding what to do with an edit happens in the loop, not here. A pull hands the open edits to your agent, and the next push applies or declines each one.

Sharing

Three cards, in the order you would ask. People with access: type an email, pick can view, can comment or can edit, and click Send invite. The person gets an email with a private link that signs them in as that address. No account needed. Under the form is everyone with access: you, then each person with their role (you can change it in place), when they last opened the document, Send again for a fresh link, and Remove, which cuts off their access right away. If email is not set up on the deployment, the dashboard gives you the link to pass on yourself.

General access is what someone can do with nothing but the document's address: Anyone with the link with a role, or Only people added. Two switches go with the role: Require an account to comment (off, a commenter types a name and an email and needs no account), and Let readers edit a private copy (a fork you never see). Changes save as you make them. The people you added keep their own role whatever is chosen here, and nothing here limits you.

Share links lists the further addresses, each with what it gives on one line, Copy, Edit… and Remove. New link… opens the dialog: a name for your own list, an optional chosen address, the role anyone holding the link gets with the same two switches, and chips for Password and Expiry, each opening its field when switched on. The dialog saves on Save, all of it at once, and a new link stays open showing its address with Copy link. The button is there on every plan and greyed out with a Pro chip on the free plan; see share links and plans.

Versions

Every push, with when it happened, whether it was Markdown, which one is live, and the network settings that version has. Older versions stay as they are. The link always points at the latest.

Controls

What the page may ask the reader for, and what it may reach on the internet. Browser features are three switches: Camera, Microphone and Location; the reader still gets the browser's own prompt. What this document may reach is an Open network switch (the page may talk to any site) and a box of hosts each for Connect to other servers and Embed other sites, one host per line. These belong to the document, not to a version: they stay when the next version is pushed and add to whatever that push asked for with --allow-connect, --allow-frame or --open. A line under them says what the current version asked for when it was pushed. A file uploaded by hand asks for nothing, so this tab is the only way it gets any of it. Everything on the tab is Pro: on the free plan each control carries a Pro tag and is switched off, and a notice links to the plan page.

Settings

Appearance is the favicon, the preview picture and the Made with Onlay badge, each with Upload or Replace and Use the default, and a switch for the badge. Part of Pro: on the free plan each row carries a Pro tag and a link to the plan page where the upload button would be.

Take down stops the link working right away, clears the cache, and deletes every version, edit and comment, which gives the storage the versions held back to your plan. It asks you twice, and it cannot be undone. A taken-down document keeps only its Edits and Versions tabs. The same happens from onlay takedown and the MCP server's takedown_document.

API keys and plan

Your plan (Free or Pro; the Plan page compares the two, shows the limits and is where you subscribe) and every key that can publish under your account. Click Create key and give it a name like laptop-cli. The key is shown once, with a Copy key button, and only a hash is kept. The table shows the name, when it was created and when it was last used, with Revoke on each live key. Keys have no scopes. Each one can publish anything under your account, and the way to limit one is to revoke it. Keys made by onlay login show up here too, named after the computer.

Connected apps

On the same page: every app that signed in through the MCP server, named after the app, with when it connected and when it was last used. Disconnect revokes it; the app is told on its next call and asks you to connect again. An app that has gone a month without use shows as expired and has to sign in again. These are not keys: nothing was copied anywhere, and there is nothing to paste.

Notifications

What Onlay emails you about, sent to the address on your account. Email me about has a switch for each kind of news, across every document: Comments and replies, Edits to your documents and New versions of documents you have open comments on. All three start on. Documents lists everything you have published with a switch each; off means you hear nothing about that document, whatever is on above. It is the same switch as the stop emails about this document link in an email, so a document muted from your inbox shows as off here and can be turned back on. If you used unsubscribe from everything in an email, the page says so and offers Start emailing me again. See email notifications for what is sent and when.

Feedback

Feedback, at the top of every page, is for telling us what broke, what confused you or what is missing in Onlay itself. It sends the note with your account and the page you were on, and we reply to the address on your account unless you switch off You can email me about this. A published document has the same link at the foot of its comments panel, Feedback; it opens this form in the dashboard, where you sign in first, because a document page runs its author's code and must not be able to send anything in your name.

Connect an app

The page an MCP client sends you to, at /authorize. It names the app that is asking and where you will be sent back, and asks you to approve or deny. Approving connects the app to your account; denying tells it no and creates nothing. Only approve an app you were just adding Onlay to.

Connect a terminal

The page onlay login opens. It shows the code your terminal printed, who is asking, and a name for the key that you can change. Only click Approve if you started it. If the code does not match your terminal, click Deny, and nothing is created. The key is only made when your terminal collects it.

Comments

Comment threads are read, answered and resolved on the page itself, in the panel next to the document, and from the CLI or the MCP server. The dashboard shows how many are open per document but does not show the threads.

Reference

Your agent, from the page

You have read the comments on your document and want your agent to deal with them, without going to a terminal. Open the document as owner, and an Agent button appears in the bottom-right corner, beside the pill that says you are signed in. Nobody but the owner sees it. Its dot is grey while no session is listening, lit while one is, and pulsing while a request is out. The first time a session shows up in a tab, the status message in the bottom-left corner says so and offers to open the panel. Click the button and the panel opens above it. If no session is listening, the panel shows how to connect each agent instead of an empty list.

The panel

  • Listening shows every agent session that is live under your account: its name, which agent it is, its working folder, whether it is idle or busy, and a has this document mark when the session already knows this document. Each row carries a mark for its agent — CC for Claude Code, CX for Codex — and the list keeps a fixed order, by agent and then by name, so rows do not move about as sessions check in. A Claude Code session is listed under the name on its terminal tab: the name you gave it with /rename, else the title Claude Code gave it, else the first thing you typed into it. A Codex listener is one row for the whole computer, named after the machine. A row marked restart to update runs an older onlay than the newest release the panel has heard of: a running process keeps the version it started with, so restart that session, or the onlay listen it runs in, after updating.
  • Tell agent to pull the feedback sends the request, with an optional note of up to 500 characters. When more than one session is live, Send to lets you pick one, or leave it to whichever has the document.
  • Requests shows the last five and where each one is: sent and waiting, picked up by which agent, pulled at what time, failed, cancelled, or expired. You can cancel a pending request. If nobody picks one up within a few seconds, the panel lets you choose a session.

How a request reaches a session

Exactly one of your sessions acts on a request, and that is decided before anything reaches a model. A request aimed at a specific session goes there. Otherwise the session that last pushed or pulled the document takes it straight away. If there is none, a session whose working folder holds a file with that document's label line takes it after a short pause, so the session with the most context wins. Otherwise the page lets you pick. Requests nobody picks up expire after a day. A Codex or T3 Code listener applies the same rules across every session or thread on the machine, by the folder each one has open, and prefers the one it sent the last request for that document to.

Your note is passed along as quoted text, never as an instruction. So even a compromised page script can only ask for a pull, nothing more. Whatever arrives, the session does the same thing: onlay pull <label>, then the loop.

Claude Code

onlay setup registers the onlay-channel server. Each request arrives in the session as a channel event with the label, the URL and the note. Channels are a research preview in Claude Code. Until Onlay is on the approved list, start your sessions with this flag so page requests reach them:

claude --dangerously-load-development-channels server:onlay-channel

A session started without the flag is not listed on the page. Claude Code starts the channel server either way, but only a session started with the flag receives its events, so the server checks how its session was launched and, without the flag, stays off the list rather than accept a request it cannot deliver. It says so in its log, and the track_document tool answers with the same words, so the agent can tell you.

Codex and the ChatGPT desktop app

Run onlay listen --codex in any terminal, once. It covers every Codex session on the computer at the same time: the CLI, the Codex view of the ChatGPT desktop app, and the IDE extension. No flag on your Codex sessions, nothing registered, nothing written to Codex's configuration. When you click Tell agent to pull the feedback, the request is queued into the running session whose folder holds the document, with the same command Codex uses when you press Tab: an idle session starts on it within a second, a busy one runs it when its current turn ends. The panel says Picked up by <session> · Codex in <folder>, then Pulled at … once the agent runs onlay pull.

The panel cannot tell whether a Codex session is idle or busy, so it shows neither. A request that no pull follows within fifteen minutes is marked failed, because Codex never confirms that it read the message. If no running session has the document's folder open, the listener stays quiet and the page says so; open the folder in Codex and click again. Two sessions in one folder: the most recently active one gets it.

T3 Code

Pair once: make a pairing link in T3 Code under Settings → Connections and run onlay setup --t3 "<link>". Then onlay listen --t3 in any terminal sends each request as a user turn into the T3 thread whose project folder holds the document. It waits for T3 Code if the app is not running.

Any other agent

Run onlay listen in the project. It prints each request as one line of JSON. Anything that can read a line of JSON and run onlay pull is an Onlay-connected agent. Sessions check in every ten seconds and drop off the Listening list thirty seconds after they stop.

Reference

Environment and files

Environment variables

VariableMeaning
ONLAY_API_KEYThe API key. Overrides the config file. Use it in CI.
ONLAY_ENDPOINTThe server to publish to. Overrides the config file. Default https://api.onlay.io.
ONLAY_NO_UPDATE_NOTICETurn off the once-a-day note about a newer release.
ONLAY_NO_LOGIN, CIMake the install scripts skip onlay login.
ONLAY_VERSION, ONLAY_BIN_DIRInstall script only: which release, and which folder.
ONLAY_AGENT_CLIENT, ONLAY_AGENT_SESSION, ONLAY_AGENT_NAMEName this agent session yourself, for an agent the CLI does not recognise. Claude Code is recognised on its own.
ONLAY_CODEX_BINThe codex executable onlay listen --codex should run, when it is not on PATH or is only there as an npm shim. CODEX_HOME is honoured the way Codex honours it.
T3CODE_HOMEWhere T3 Code keeps its state, honoured the way T3 honours it. Default ~/.t3. This is where onlay setup and onlay listen --t3 look for the running server.

Files

PathWhat
~/.onlay/config.jsonapiKey and, if set, endpoint. Mode 0600.
~/.onlay/bin/The program. %LOCALAPPDATA%\onlay\bin on Windows.
~/.onlay/affinity.json, update.jsonWhich session has which document, for page requests. When the update notice was last shown.
~/.onlay/t3.jsonThe pairing with T3 Code, written by onlay setup --t3. Mode 0600.
.onlay/attachments/<label>/Images pulled for a document, inside the project. Hidden from git locally.
~/.claude/skills/create-editable-documents-with-onlay/SKILL.mdThe skill for Claude Code, written by onlay setup. onlay init writes the same file under a project, in .claude/skills/ or .agents/skills/ as this computer's agents call for.
~/.claude/settings.jsonSix permission rules, added to what is there. Backup at settings.json.onlay-backup.
~/.agents/skills/create-editable-documents-with-onlay/SKILL.mdThe same skill for Codex, the ChatGPT desktop app and Cursor, written by onlay setup.
~/.codex/rules/onlay.rulesCodex command rules allowing onlay push, pull and update. Written by onlay setup; delete it to take them away.
~/.cursor/cli-config.jsonOne permission rule, Shell(onlay), added to what is there. Backup at cli-config.json.onlay-backup.
~/.onlay/t3.jsonThe T3 Code pairing, written by onlay setup --t3. Mode 0600.

Domains

HostWhat
<label>.onlaycontent.comA published document. This is a separate domain from the app, with one subdomain per document, and search engines never index it. Documents cannot read each other or your session.
app.onlay.ioThe dashboard.
api.onlay.ioWhere the CLI publishes and pulls, and the MCP server at /mcp.
get.onlay.ioThe install scripts and releases.