# Document programs

> How a document asks a local program to do work, how to attach one from the terminal, and what the approval covers.

Source: https://htmlclay.com/docs/programs/

A document program is a command on your computer that a page asks to do work by sending it a request. The program edits the file on disk with whatever tools it already has, and the change reaches the page like any other file change. In code and error text, HTML Clay calls a document program a helper.

## Before you start

You need HTML Clay installed and a file that opens through it. [Get started](https://htmlclay.com/docs/guide/quick-start/) covers that setup. The shell example below is for macOS and Linux. Install ripgrep and jq first. Check them in your login shell, whose PATH HTML Clay uses for document programs:

```bash
$SHELL -lc 'rg --version && jq --version'
```

The command prints a version line for each tool.

On Linux, document permission prompts also need `zenity` or `kdialog`. On Windows, a document program must be a `.exe`, `.bat` or `.cmd` file.

Only connect a program you trust with your account. It runs as you and can read or change any file you can access.

## Attach from the terminal

This example searches the saved HTML of a small todo list. Searching does not change the file.

Save this page as `todo.htmlclay` in a folder inside your home folder, such as `~/Documents/todo`:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Todo</title>
</head>
<body>
  <h1>Todo</h1>
  <ul>
    <li class="todo" data-done="false">Buy milk</li>
    <li class="todo" data-done="false">Book dentist</li>
  </ul>
  <script src="https://clayjs.com/v1/clay.js?plugins=wire"></script>
  <script>
    clay.ready.then(() => {
      const panel = document.createElement("section");
      panel.setAttribute("clay", "no-save no-snapshot no-watch");
      panel.innerHTML = `
        <form>
          <label>Search the saved HTML <input name="query" required></label>
          <button>Search</button>
        </form>
        <pre aria-live="polite"></pre>
      `;
      document.body.append(panel);
      const form = panel.querySelector("form");
      const output = panel.querySelector("pre");
      const button = panel.querySelector("button");

      form.addEventListener("submit", async (event) => {
        event.preventDefault();
        const query = form.elements.query.value.trim();
        if (!query || button.disabled) return;
        button.disabled = true;
        output.textContent = "Searching";
        const request = clay.wire.send({ query }, { document: "none" });
        const outcome = await request.done;
        output.textContent = outcome.state === "done"
          ? outcome.result.matches.join("\n") || "No matches"
          : outcome.error || outcome.state;
        button.disabled = false;
      });
    });
  </script>
</body>
</html>
```

The `wire` plugin supplies `clay.wire.send`. The script builds the search controls each time the page opens. Their `clay` attribute keeps the controls and results out of saves. `document: "none"` skips saving the page before searching, so the search reads the saved file without any unsaved page edits.

Save these five lines as `search.sh` beside the document:

```bash
#!/bin/sh
query=$(jq -er '.payload.query | select(type == "string" and length > 0)') || exit 1
matches=$(rg --no-heading --no-filename --color never -F -- "$query" "$HTMLCLAY_WIRE_FILE")
code=$?; [ "$code" -le 1 ] || exit "$code"
jq -cn --arg matches "$matches" '{type:"result",value:{matches:($matches | split("\n") | map(select(length > 0)))}}'
```

HTML Clay supplies the document's path in `HTMLCLAY_WIRE_FILE`. The program reads `payload.query` from the request. Ripgrep searches for that literal text and returns matching source lines. Exit code 1 means no matches, so the script returns an empty list. Other ripgrep failures stop the program.

Double click `todo.htmlclay` to open it. In your terminal, change to the folder containing both files. On macOS, first make the app's command available in that terminal:

```bash
alias htmlclay="/Applications/HTMLClay.app/Contents/MacOS/htmlclay"
```

Skip the alias on Linux. Make the script executable, then attach it:

```bash
chmod +x search.sh
htmlclay wire serve todo.htmlclay --protocol=jsonl -- ./search.sh
```

Leave that terminal running. HTML Clay starts `search.sh` once per request. `--protocol=jsonl` tells HTML Clay to read one JSON object per output line. Without that flag, output lines are plain progress text and no JSON result reaches the page.

Type `milk` and press Search. With the file exactly as shown, the page displays:

```
    <li class="todo" data-done="false">Buy milk</li>
```

You are seeing a line from the saved file, including its HTML tags. Search for `carrots` and the page displays `No matches`.

## Let the document name its program

The same search can run without an open terminal. The document names a program, and you choose which executable that name refers to.

Stop `wire serve` with Ctrl+C. Add this program name near the start of the document's `<head>`:

```html
<meta name="htmlclay-helper" content="todo-search">
```

Replace the page's `clay.wire.send` line with this named request:

```js
const request = clay.wire.send({ query }, { helper: "todo-search" });
```

Save the file and open it directly again by double clicking it. HTML Clay asks "Allow document programs?" before the page's script runs. On macOS and on Linux with zenity, the prompt offers two allow buttons:

1.  "Allow for This Document" remembers permission for this document, including after a restart.
2.  "Allow for Any Document" lets every document declaring that name use the selected program.

On macOS, "Deny" is the default button. Return and Escape also refuse access. A refusal lasts until you close the page and is never stored. Double clicking the file again asks again.

Choose "Allow for This Document". With kdialog, choose "Yes". If `todo-search` has no registered program, the picker opens with "Choose the program for todo-search". Select `search.sh`. The page then opens.

Search for `milk` again. HTML Clay starts the selected program and returns the same result.

The tray's Programs menu lists your registered programs. Use it to add a program or manage an existing program's access.

## Read the result on the page

The result becomes available when `request.done` resolves. For the search above, the program prints this record:

```json
{"type":"result","value":{"matches":["    <li class=\"todo\" data-done=\"false\">Buy milk</li>"]}}
```

The record's `value` becomes `outcome.result`. Check `outcome.state` before reading it. The `done` promise resolves for errors and cancellations too; it never rejects.

Named requests use `document: "none"` by default, so both attachment methods search the saved file.

A program can also edit the document itself. Use `document: "edit"` for that request and load the `sync` plugin alongside `wire`. ClayJS saves the page before sending the request and holds autosave until the request ends. [Agent editing](https://htmlclay.com/docs/guide/let-a-program-edit/) has a complete example. [Live updates](https://htmlclay.com/docs/live-sync/) explains how file changes reach the open page.

## Access and limits

`clay.wire.send` sends to the page's own file. A trusted folder lets HTML Clay open every `.htmlclay` file in it for editing. Any other file in that folder can reach this file's approved program.

Following a link to a file in a trusted folder does not set up its programs. Double click the file to set up its programs.

`document: "none"` only skips saving the page before the request. It does not stop a program from writing files.

Each file can have one attached program at a time. Stop a terminal attachment before switching to document programs.

Programs started with `--protocol=jsonl` have five minutes to finish, regardless of progress. A result record can contain at most 512 KiB, including its JSON wrapper. Keep answers small or divide them into separate requests.

## If the search does not run

1.  "open it first" or "has never served this file's folder" in the terminal: open `todo.htmlclay` through HTML Clay, then attach again.
2.  "no agent is attached to this file" on the page: check that `wire serve` is still running. For a document program, check for a missing or misspelled `<meta name="htmlclay-helper">` tag.
3.  "was not granted that helper" on the page: check the program declaration. Double click the file to be asked again.
4.  The page stays on "Searching" and the Search button stays disabled: stop `wire serve`, add `--protocol=jsonl`, restart the command and reload the page. Reserve the program's standard output for JSON records. Send diagnostic text to standard error.

For request states, cancellation and the complete output format, read the [wire protocol reference](https://github.com/panphora/htmlclay/blob/main/docs/wire.md). To read or change selected document content from a local program without a page request, use the [JSON API](https://htmlclay.com/docs/json-http/).

## Registered helper programs

A document declares helpers in `<meta name="htmlclay-helper">` elements near the start of its `<head>`. HTML Clay reads at most the first 512 KiB and accepts at most eight valid, distinct names. Later declarations are silently ignored.

The decision happens when HTML Clay receives a direct file open, before that page's script runs. For every name with no stored decision, HTML Clay raises one native dialog. The dispatcher never raises a permission dialog in response to a request. A denied helper call fails immediately.

A linked `.htmlclay` file opened inside a trusted folder registers without a prompt. It gets the helpers its existing allows already cover, and any name without one stays unavailable until the file is opened directly.

The dialog offers two affirmative choices. Neither is a one-time grant:

- **"Allow for This Document"** allows the selected program for that one document, across reloads and restarts.
- **"Allow for Any Document"** allows the selected program for every document that declares the same helper name. It is not limited to one folder.

The tray's row for a program turns any-document access back off and forgets the documents allowed to use it.

The prompt shows, for each name, the path of the program that would run, and marks a registered path that no longer exists as missing. A name with no registered program says so, and an Allow opens a file picker to choose one. When several registered programs share a name, the prompt lists every path and an Allow opens the file picker to choose which one runs; picking a file already registered under that name reuses its registration. HTML Clay never chooses between them itself: if more than one of them is allowed for any document, the name stays undecided until a direct open asks. The saved decision identifies one concrete program by its local ID.

A refusal is never stored. Deny, Return, Escape, closing the dialog, the 120 second timeout, a missing dialog tool and any dialog error all leave the document without those helpers for this open, and the next direct open asks again. A refusal holds for the rest of that open, including across tray changes and navigation. Deny is the keyboard default, so Return and Escape never grant access. Following a link to the document never shows the prompt, so it cannot repeat without you. Refusals stored by 1.9.x are dropped when HTML Clay starts; if exactly one program with that name is allowed for any document, a document that had refused it now uses it.

### What approval gives the program

For each request, HTML Clay starts the registered path with the document's folder as its working directory. It passes the full request as JSON on standard input, inherits the app's environment, and supplies the document path and request ID in environment variables. On macOS and Linux it also supplies a login shell `PATH` so ordinary script interpreters can be found.

The five minute execution deadline, the structured output parser, and the result size limits protect HTML Clay from an unbounded request or malformed response. They do not restrict the program's operating system access. A helper can, among other things:

- Read, create, change, or delete any file your account can access, including files outside the document folder and files HTML Clay itself refuses to serve.
- List directories, start other programs, and leave descendant processes running.
- Use the network and send away anything it can read.
- Read or change HTML Clay's `config.json`, including its program registry and permission decisions. The file is mode 0600, but the helper runs as the same account that owns it.

HTML Clay permits up to eight concurrent helper requests per document. This is not an app wide limit. Several documents can each run eight children, so the limit is not resource isolation.

Register only programs you trust with your account. The security boundary inside a helper is what that program does with hostile standard input. Validate the operation, types, paths, sizes, and every other value before using them. HTML Clay validates the helper's output protocol. It cannot validate the meaning of the input for the program.

HTML Clay stores the selected program path, not a hash or a pinned file identity. Replacing the program at that path, or changing the target behind a selected symlink, changes what runs without another prompt. Whoever can update that program controls the code future requests execute.

### Who can invoke an approved program

The dispatcher accepts only names declared by that document and allowed in its stored decisions. Helper wire sends and subscriptions also require that document's save token. Another HTML Clay origin and a local process without the token are refused.

Files in one trusted folder share an origin. A hostile file in that folder can open an approved sibling in a visible top level tab, read the sibling's save token through their shared origin, and use that token to reach the sibling's approved helper. Document specific approval therefore does not isolate a helper from hostile files in the same trusted folder.

When a document has a helper dispatcher, both live sync relay addresses refuse page supplied updates to that target, on both relay lanes. This prevents a sibling from silently pushing a script into an already open, helper enabled tab. It does not stop the visible navigation and token path above, and it does not limit the helper after execution starts.

### Removing or revoking a program

Removing a program or its document permissions stops future dispatch. HTML Clay advances the document's helper generation, rejects late output, and cancels work it is tracking.

That cancellation is not containment. The runner signals only the direct child process and waits for it. Descendants can survive, including descendants that keep output pipes open. Process group cleanup, if added, would be tidying rather than a security boundary because a program running as the user can escape the group or arrange work elsewhere.

Removing or revoking a program does not undo writes, recover data already sent away, or stop work that escaped the tracked process. It also cannot protect the registry from that program, because the registry is owned by the same account.

Removing a program leaves every document decision that pointed at it in place. Each one reads as undecided while the program is gone, so the next open asks again, and restoring the same program ID makes those decisions grant once more. Registering a new program under the same display name does not inherit them: registration mints a new ID, and the stored decisions name the old one.

## Let any local program edit the open file

The wire is a per file channel between an open page and one command you started yourself.

```
htmlclay wire serve <file> -- <cmd>
```

### Your program reads stdin, prints progress, and exits 0

The command receives a small JSON request on stdin. Each line it prints becomes a progress update that the page can show. Exit 0 means the work is done. The program edits the file on disk with whatever tools it already has. The command `claude -p "$(cat)"` works as an AI handler without extra code.

1. **Page asks** — Opaque JSON request
2. **Program edits** — File on disk
3. **Page updates** — Ordinary file change

The wire carries JSON requests and responses. AI editing includes the selected HTML, your request and any included context; your local agent may send that content to its model provider. Requests are capped at 1 MiB.

### Built to be trusted with your files

**An attached handler works without an open tab.** It keeps watching the file, and edits are still versioned. HTML Clay enables history before the handler's first write. If the file cannot be read, it refuses to attach the handler instead of letting it run without versioning.

**A browser cannot act as the handler.** Request headers that a browser cannot suppress tell HTML Clay whether a request came from a browser or a local process. Anything that identifies itself as a browser is refused the handler role.

**If no program is attached, the page is told right away.** The send response reports whether a handler queued the request, so the page cannot mistake its own echo for delivery.

The full protocol is in [docs/wire.md](https://github.com/panphora/htmlclay/blob/main/docs/wire.md).
