---
title: memhtml session exec
description: "Run a script over the session's view (head plus overlay) in a writable sandbox and harvest its writes into the overlay."
---

## 1. Synopsis

Run a script over the session's view (head plus overlay) in a writable sandbox and harvest its writes into the overlay.

```sh
memhtml session exec [options]
```

## 2. Arguments

This command takes no positional arguments.

## 3. Flags

| Flag | Type | Default | Values | Description |
| --- | --- | --- | --- | --- |
| `--id` | string | *no default* | *no fixed set* | The session whose view the script sees. Defaults to $MEMHTML_SESSION; with neither, the call is ERR_MISSING_ARGUMENT. With the id taken from $MEMHTML_SESSION and no log yet, the session is started on refs/heads/main's tip first, as `session put` does. |
| `--file` | string | *no default* | *no fixed set* | The script, as a path on the HOST. Omit it, pass `--file -`, or a positional `-` to read stdin, which is the usual door: a heredoc (`memhtml session exec --id s1 <<'SH'` ... `SH`) delivers the script with no quoting to get wrong. Mutually exclusive with `--script`. A new file the script writes is held to the write bar `session put` states; one below it blocks the whole harvest. An existing file whose article is unchanged may gain or lose `<link rel="memhtml-...">` lines and `<meta name="memhtml-entity">` lines in its head: each becomes a `link`, `unlink`, `label`, or `unlabel` op, and any other head edit is rejected with a reason. A file copied unchanged to `archive/<YYYY>/<its path>` and removed from its old one becomes an `archive`, and one copied unchanged to exactly one new live path a `move`, which keeps its bytes and its content hash; either way, every edge a live record carries to the old path is repointed to where the file went in the same harvest (an `unlink` of the edge and a `link` of the same rel), so the script need not edit the records that cite it. New files the script writes are not repointed: one that supersedes a record the same script archives names it at `/archive/<YYYY>/<its path>`, and one that names the old path blocks the harvest. An edge on a record the session may not write (an arc or a people record, outside a curate/ ref) is left for the curator after an archive, and blocks a move. A file removed with no copy is rejected: deletion is not an operation. |
| `--script` | string | *no default* | *no fixed set* | The script source, inline. Mutually exclusive with `--file` and with stdin. |
| `--lang` | string | `bash` | `bash`, `js` | The script's language, echoed as `lang` in the report. `bash` runs it as a shell program: `cat`, `grep -r`, `sed -i`, `find` (with `-exec`), `xargs`, `jq`, `awk`, pipes, redirection, and nested heredocs all work on the corpus at `/mnt/memhtml`, so `cat > /mnt/memhtml/projects/<slug>/<name>.html <<'HTML'` ... `HTML` writes a record and a `sed -i` that splices a `<meta name="memhtml-entity">` line into a head labels one. Use absolute paths. `js-exec <file>` runs a JavaScript module from inside a bash script. `js` runs the whole script as a JavaScript module through `js-exec` with `/workspace/lib/corpus.mjs` preloaded, as `memhtml exec` does. The mount, the timeout, and the harvest are the same for both. There is no network in either: `curl` is not a command. |
| `--timeout-ms` | integer | `30000` | *no fixed set* | Wall-clock bound on the script. Exceeding it is `exitCode` 124 with `timedOut: true`, for a bash loop and a `js-exec` module alike. Other sandbox bounds end a runaway sooner with exit 126 and the bound named on stderr: call depth 100, and 100,000 iterations inside one `awk`, `sed`, or `jq`. A value above 600000 is refused. Under the head server a run still going 5 s past the bound (a single command the shell cannot interrupt) has its worker stopped, and it reports exit 124 with nothing harvested. |
| `--server` | flag | `true` | *no fixed set* | Ask the head server on .memhtml/head.sock to run the script (`memhtml head serve`); `--no-server` always runs it here. The server reads the session's log, takes the version at the session's base when that is its ref's tip or an ancestor of it, runs the script in a worker, harvests, validates, and appends exactly as a local run does, one exec per session at a time; the payload's `head.source` is `server` and `server.base` says `tip` or `ancestor`. The script runs here instead when the connection to a server never completes (no socket, nobody listening, any connect error, no accept within 100 ms, or a socket path past the platform's limit, which is never dialed), when the server answers anything but 200, or when it declines (`server.reason` `base-unservable` or `busy`); `server.reason` names why (`disabled` under `--no-server`, `no-server`, `refused`). When the server took the script and its answer was lost, the call fails with ERR_STORAGE rather than run it twice. Either way the request body (the script as JSON) is capped at 1,048,576 bytes: a larger script is ERR_INVALID_FLAG at exit 2 before anything runs. |

Every command also accepts the [global flags](/reference/global-flags/).

## 4. Response

On success the command writes one JSON envelope to stdout, and its `type` is `session.exec.report`.

On failure it writes the failure envelope, whose `code` comes from [error codes](/reference/error-codes/).

[The JSON envelope](/reference/envelope/) gives the fields both shapes carry. [Response types](/reference/response-types/) lists every value `type` takes across the binary.

## 5. Further reading

The guide blocks that name this command:

- [`write-surfaces`](/reference/guide/write-surfaces/)

## 6. Provenance

A loader generates this page from `apps/cli/src/commands.ts` while the site builds, so no file in the repository holds it: the `COMMANDS` array there is the one source of argument parsing, of `memhtml manifest`, and of `AGENTS.md`. Change the registry and this page changes with it.