rescriptum
Reference
Guide
Reference
Reference
The exhaustive counterpart to the rest of the guide. Tables and contracts, one page per surface:
- Configuration — every environment variable, its default, and what an invalid value does.
- HTTP surface — methods, status codes, headers, limits.
- Formats and endpoint aliases — which extension is which format, and which URL segment asks for it.
- Command line — every subcommand and flag.
Looking for the narrative instead? Start at what rescriptum is.
Configuration
Configuration
Environment variables — and, optionally, a file to read them from, in either of two shapes. There is no command line to get wrong, and the variables are the whole configuration: both file formats set exactly the same things under exactly the same rules, so nothing you can write in a file means anything the environment could not.
The variables
| Variable | Default | Meaning |
|---|---|---|
RESCRIPTUM_CONFIG | unset | Read defaults from this TOML file — see below |
RESCRIPTUM_ENV_FILE | unset | Read defaults from this KEY=value file — see below |
RESCRIPTUM_STORE | files | files (a directory) or sqlite (a database) |
RESCRIPTUM_ANSWERS_DIR | /srv/answers | Directory of answer documents |
RESCRIPTUM_DB_PATH | /srv/answers.db | Database path, when RESCRIPTUM_STORE=sqlite |
RESCRIPTUM_LISTEN_ADDR | 0.0.0.0:8000 | Listen address. :0 picks a free port, and the bound one is printed |
RESCRIPTUM_WORKERS | CPU count | Async runtime threads. Not a concurrency limit |
RESCRIPTUM_MAX_CONNECTIONS | 2048 | In-flight connections before shedding with 503 |
RESCRIPTUM_TIMEOUT_SECS | 10 | Header-read timeout and whole-connection deadline |
RESCRIPTUM_ANSWER_TOKEN | unset | Bearer token the answer endpoint requires. Unset means open |
RESCRIPTUM_ADMIN_ADDR | unset | Admin API listener. Unset means the admin API is off |
RESCRIPTUM_ADMIN_TOKEN | unset | Admin bearer token, 16+ characters. Required with RESCRIPTUM_ADMIN_ADDR |
RESCRIPTUM_CAPTURE_DIR | unset | Record request bodies here. Unset means no capture |
RESCRIPTUM_LOG | all | all, problems or off — see below |
RESCRIPTUM_LOG_FILE | unset | A file to append to, or stdout / stderr. Unset means stderr |
RESCRIPTUM_MEDIA_DIR | unset | Installer images. Unset means no media and no media listener |
RESCRIPTUM_MEDIA_ADDR | 0.0.0.0:8001 | The media listener, when there is a media directory |
RESCRIPTUM_MEDIA_TIMEOUT_SECS | 600 | Whole-transfer deadline. Deliberately not the answer listener’s 10 |
RESCRIPTUM_MEDIA_MAX_CONNECTIONS | 16 | Concurrent transfers. Low on purpose: each holds its permit for minutes |
RESCRIPTUM_PUBLIC_HOST | derived | The host generated URLs name. A host, never a URL |
RESCRIPTUM_BOOT_ALLOW | unset | Client CIDRs allowed to fetch boot media. Unset means anyone who can reach the port |
RESCRIPTUM_BOOT_DIR | unset | Loaders and menus, handed out over TFTP. Unset means no TFTP at all |
RESCRIPTUM_TFTP_ADDR | 0.0.0.0:69 | The TFTP listener, or off for none. Port 69 is privileged; see RESCRIPTUM_USER |
RESCRIPTUM_TFTP_PORT_RANGE | unset | The ports transfers answer from, as first-last. A TFTP transfer leaves port 69 immediately — the server replies from a fresh port and the client acknowledges to that — so a firewall allowing only 69 drops the acknowledgement and the machine looks like it lost interest. Pin the range so it can be opened. Unset, the kernel picks |
RESCRIPTUM_TFTP_BLKSIZE | 1468 | The largest TFTP block to agree to. 1468 fills a 1500-byte path exactly — 1468 payload, 4 TFTP, 8 UDP, 20 IP — so a VLAN tag or a tunnel makes the frame too big and a PXE ROM usually just stops. Lower it (1400, or 512) when a boot stalls at the first block |
RESCRIPTUM_BOOT_TIMEOUT_SECS | 15 | Seconds before the menu falls through to local boot |
RESCRIPTUM_BOOT_UNCLAIMED | menu | What a machine no answer claims gets. local hands it back to its firmware instead, which inverts what an answer file means: present is install this one rather than leave this one alone |
RESCRIPTUM_INSTALLED_TOKEN | unset | Proxmox’s [post-installation-webhook] token. Set it and POST /installed exists, dropping a machine’s install claim when it reports success. Unset, there is no endpoint |
RESCRIPTUM_BOOT_LOGO | built-in | A PNG to show behind the menu |
RESCRIPTUM_BOOT_TITLE | built-in | The menu’s title bar |
RESCRIPTUM_USER / _GROUP | unset | Drop to these after binding. The other order fails on deployment |
/srv is where the filesystem hierarchy standard puts data served by the system, which is
what an answers directory is. Both defaults live there so that a bare rescriptum does
something plausible on any Linux host. Nothing creates the directory for you, and the server
says so at startup if it is missing.
Logging
One line per event, on stderr by default. Two knobs, because the two questions are different.
What — RESCRIPTUM_LOG:
| Value | Keeps |
|---|---|
all (default) | every request, plus startup, warnings and errors |
problems | startup, warnings, errors, and only the requests that did not succeed |
off / none | nothing at all |
A successful answer is one line, and at thirteen thousand requests a second that is the
only thing here with any volume. problems is what you want once a rollout is routine and
the disk is not. Everything else is low-volume and diagnostic, so it survives both.
A request that never reached a status at all — a connection that timed out mid-body —
counts as a problem. An unrecognised value falls back to all with a warning: a typo must
not be the reason nobody can see why a rollout failed. The level is named in the startup
line (log=problems), so an empty log explains itself.
Where — RESCRIPTUM_LOG_FILE:
| Value | Goes to |
|---|---|
unset, or stderr | stderr, which is what a supervisor reads |
stdout | stdout |
| any other value | that file, appended to; parent directories are created |
A file that cannot be opened is a startup error, not a fall back to stderr — that would be a silent surprise discovered much later. A write that fails once the server is running is dropped instead: a provisioning server that died because its log disk filled up would fail every install in flight in order to report that it could not report something.
Rotation is yours. Under systemd there is nothing to do, since the log goes to the journal;
with a file, point logrotate at it with copytruncate.
The TOML file
RESCRIPTUM_CONFIG names a file in TOML that sets the same variables in a shape meant to
be read. Reach for it when a person edits the file by hand — on a NAS, in File Station
or over SMB — which is exactly where RESCRIPTUM_ANSWERS_DIR=… on every line reads
poorly and where the word “environment” sends people looking for a shell that is not
there.
# /etc/rescriptum.toml (chmod 600, owned by root)
answers_dir = "/srv/answers"
listen_addr = "0.0.0.0:8000"
log = "problems" # all | problems | off
[store]
kind = "sqlite"
db_path = "/srv/answers.db"
[server]
workers = 2
max_connections = 2048
timeout_secs = 10
[admin]
addr = "127.0.0.1:8001"
token = "…"
[answer]
token = "…"
capture_dir = "/var/lib/rescriptum/captures"
$ RESCRIPTUM_CONFIG=/etc/rescriptum.toml rescriptum
2026-08-29T12:42:02Z - reading configuration defaults from /etc/rescriptum.toml (8 set)
Every rule the env file has, this one has too: never discovered, only named (there is
no ./rescriptum.toml); the real environment wins; and a file that was asked for and
cannot be read is a startup error, never a warning.
Put it outside the answers directory. Every servable .toml at the top of that
directory is an answer document, and this format shares the extension — a configuration
file dropped in there is reported by check as a misplaced answer, and migrate offers
to move it. /etc is the obvious home; on a packaged install the package chooses one.
The names
The prefix goes away and tables do the grouping. Nothing else changes: each line below is
the variable of the same name, and rescriptum config prints both spellings.
| In the file | Variable |
|---|---|
answers_dir | RESCRIPTUM_ANSWERS_DIR |
listen_addr | RESCRIPTUM_LISTEN_ADDR |
log, log_file | RESCRIPTUM_LOG, RESCRIPTUM_LOG_FILE |
public_host | RESCRIPTUM_PUBLIC_HOST |
user, group | RESCRIPTUM_USER, RESCRIPTUM_GROUP |
store.kind, store.db_path | RESCRIPTUM_STORE, RESCRIPTUM_DB_PATH |
server.workers, server.max_connections, server.timeout_secs | RESCRIPTUM_WORKERS, RESCRIPTUM_MAX_CONNECTIONS, RESCRIPTUM_TIMEOUT_SECS |
admin.addr, admin.token | RESCRIPTUM_ADMIN_ADDR, RESCRIPTUM_ADMIN_TOKEN |
answer.token, answer.capture_dir | RESCRIPTUM_ANSWER_TOKEN, RESCRIPTUM_CAPTURE_DIR |
media.dir, media.addr, media.timeout_secs, media.max_connections | the RESCRIPTUM_MEDIA_* four |
boot.dir, boot.allow, boot.unclaimed, boot.timeout_secs, boot.logo, boot.title | the RESCRIPTUM_BOOT_* six |
tftp.addr, tftp.port_range, tftp.blksize | the RESCRIPTUM_TFTP_* three |
installed.token | RESCRIPTUM_INSTALLED_TOKEN |
The format
| Any TOML scalar | a number may be written as a number (workers = 2) or as a string; both reach the server as the same setting |
A # comment | anywhere, including at the end of a line — unlike the env file, which has no escapes and so cannot have inline comments |
A value with a #, a quote or a space | fine, quoted the way TOML quotes things, and read back unchanged |
"" | unset — the same rule as an exported-but-empty variable, which is what lets config unset empty a line instead of deleting the paragraph that documents it |
| The same key twice | refused by TOML itself, so the file does not load |
| A key this program does not read | a warning naming it, so admin.tokenn is caught rather than ignored |
| A list or a table where a value belongs | a startup error: unlike a misspelling it was aimed at a real setting, and serving the default while the file says otherwise would be silent |
| A file others can read | a warning with its mode, because it may hold admin.token |
Warnings name keys and paths, never values.
Both files at once
Naming both is a transition rather than a steady state, so nothing is refused and the
order is stated at startup: the environment beats the TOML file, which beats the env
file. rescriptum config shows which of the three put every value in force, and
config set writes to the TOML file — the one the server reads first, so that a write
cannot be a change that silently does nothing.
The env file
RESCRIPTUM_ENV_FILE names a file of the same variables. It exists for deployments with
nowhere good to put a token — chiefly Synology DSM 7, which has no systemd. Under
systemd, EnvironmentFile= already does this and you do not need it.
# /etc/rescriptum.env (chmod 600, owned by root)
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/srv/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
$ RESCRIPTUM_ENV_FILE=/etc/rescriptum.env rescriptum
2026-08-24T12:42:02Z - reading configuration defaults from /etc/rescriptum.env (4 set)
It is never discovered, only named. There is no ./.env. This binary runs as root: if
it picked a file up from whatever directory it happened to be launched in, anyone who could
write there would own RESCRIPTUM_ADMIN_TOKEN — and with it the root password of every
machine installed afterwards.
The real environment wins. The file supplies defaults, so something exported deliberately at launch is never silently overridden. An exported-but-empty variable counts as unset, so the file still applies.
A file that was asked for and cannot be read is a startup error, not a warning. That is the whole point: the failure it replaces is a server coming up on its defaults — wrong answers directory, no admin token — without a word in the log.
The format
KEY=value, one per line | leading export is accepted, so the same file can also be sourced |
# at the start of a line | a comment |
# anywhere else | part of the value. There are no inline comments: truncating a token at a # it legitimately contains would be silent, and a comment landing in a value is loud |
"quoted" or 'quoted' | the quotes are stripped and inner whitespace is kept; unquoted values are trimmed |
$HOME, ${x} | not expanded. This is not a shell — no substitution, no continuation lines |
| the same key twice | a startup error, rather than a guess about which was meant |
| a key this program does not read | a warning naming the key — so RESCRIPTUM_ADMIN_TOKENN is caught rather than ignored |
| a file others can read | a warning with its mode, because it may hold the admin token |
Warnings name keys and paths, never values.
Reading and editing it
rescriptum config prints every variable, its value, and which of the files and the
environment put it there — the distinction that matters, because the files supply
defaults and the real environment wins. config set edits the file the way you would want
it edited, in either format: comments kept, a commented-out setting uncommented in place
rather than duplicated (in the env file) or the value replaced where it stands (in TOML),
and a change that would leave a server unable to start refused before anything is
written. It is documented in the command line reference, and it is
what the DSM application drives
underneath.
Invalid values
| Case | What happens |
|---|---|
Exported but empty (RESCRIPTUM_LISTEN_ADDR=) | treated as unset — an empty value is a mistake, not an instruction |
| Whitespace-only | same, and values are trimmed |
| A zero or unparseable number | falls back to the default, rather than starting a server that accepts connections and never answers |
RESCRIPTUM_STORE set to anything else | a warning, and files is used |
RESCRIPTUM_ENV_FILE or RESCRIPTUM_CONFIG naming a missing, unreadable or malformed file | a startup error |
| A TOML setting given a list or a table | a startup error, unlike a misspelled key, which warns |
RESCRIPTUM_STORE=sqlite on a binary built without the feature | a startup error |
Startup errors
These stop the server rather than warning, because starting anyway would be worse:
| Condition | Why it is fatal |
|---|---|
RESCRIPTUM_ADMIN_ADDR set with RESCRIPTUM_STORE not sqlite | two ways to change the same configuration, racing |
RESCRIPTUM_ADMIN_ADDR set with no RESCRIPTUM_ADMIN_TOKEN | an open API that rewrites root credentials |
RESCRIPTUM_ADMIN_TOKEN under 16 characters | short enough to guess |
| The listen address cannot be bound | nothing to do |
| The store cannot be opened | nothing to serve |
RESCRIPTUM_MEDIA_ADDR set with no RESCRIPTUM_MEDIA_DIR | a listener with nothing to serve |
RESCRIPTUM_MEDIA_ADDR equal to the answer or admin address | the second bind loses, and which one depends on start order |
RESCRIPTUM_PUBLIC_HOST carrying a scheme, a port or a path | it is written into URLs for two listeners; one port in the value pins every generated script to one of them |
RESCRIPTUM_TFTP_ADDR set with no RESCRIPTUM_BOOT_DIR | a listener with no loaders to hand out |
| The boot directory cannot be resolved | every path check compares against it |
RESCRIPTUM_USER names an account that does not exist | nothing to become |
Startup warnings
These are printed and the server carries on:
| Condition | Line |
|---|---|
| Answers directory missing | warning: … does not exist yet — every request will 404 until it does |
| Answers path exists but is not a directory | warning: … is not a directory — every request will 404 until it is |
| Answers directory present but unreadable | warning: … cannot be read: … — every request will 404 until that is fixed. The likeliest cause is the server running as a user that is not the directory’s owner |
| Admin API not on loopback | warning: the admin API is not bound to loopback — … |
RESCRIPTUM_ANSWER_TOKEN under 16 characters | a warning, not an error — refusing to start would leave a fleet unable to install |
| Any problem in the answer set | one warning: line each, the same set check reports |
RESCRIPTUM_PUBLIC_HOST unset | The routing table’s answer, or the sole interface address when there is no default route. Logged either way, as a warning naming the other addresses when there are any. A NAT host still gets it wrong silently |
| TFTP cannot bind | warning: cannot bind TFTP on … — the one listener whose failed bind is not fatal. Port 69 is the only privileged port in the design, so it is the only bind that can fail for something nobody configured; answers are the product, and dying would fail every install in flight to report that a second port could not be opened. boot check exits non-zero and the message names the ways to have the port |
| Media directory missing or unlistable | one warning: media: … line — a fleet must never be unable to install because one image is odd |
Compile-time options
| Feature | Default | Effect |
|---|---|---|
sqlite | on | The SQLite store and the admin API |
boot | on | The media catalogue, the ISO reader and the media listener |
Measured on ARMv7 (gnueabihf, glibc floor 2.17), all four in one sitting on 2026-08-29. Re-measure rather than quoting these: they moved by about 375 KB when that target changed from musl, and the set they replace here had drifted about 200 KB out of date.
| Build | Bytes |
|---|---|
| both (default) | 2,813,712 |
sqlite only | 2,557,592 |
boot only | 1,649,048 |
| neither | 1,392,544 |
Fixed limits
Not configurable, and deliberately so:
| Limit | Value | Where |
|---|---|---|
| Request body | 1 MB | answer endpoint — an aberrant Content-Length is refused from the header |
| Document size | 256 KB | admin API PUT |
| Captured requests | 1000 captures | counted from the directory at startup, so a restart does not start again |
| Admin failures before a block | 5 within 60 s | block doubles to a maximum of 900 s |
| Addresses tracked by the guard | 4096 | so the guard cannot be turned into a memory leak |
| Listing reload backstop | 1 s | forces a re-read even when the directory mtime looks unchanged |
HTTP surface
HTTP surface
Two listeners, and they never share a port. The answer endpoint is what installers talk to; the admin API is off unless configured.
The answer endpoint
| Request | Response |
|---|---|
POST any path | the answer, content-typed by its format |
GET any path | the same |
GET /health | 200 OK, body OK\n — no token needed, never rate-limited |
| any other method | 405 |
Any path, because the URL is baked into an ISO and this server does not get to choose
it. The path is not ignored, though: segments naming a
format alias restrict which documents may answer, and the path also
contributes the path, file and segment
facts.
Status codes
| Code | When |
|---|---|
200 | an answer applied |
400 | the body could not be read |
401 | RESCRIPTUM_ANSWER_TOKEN is set and the request did not present it |
404 | nothing claimed the request and there is no default for the format asked for |
405 | a method other than GET or POST |
413 | body over 1 MB, or a Content-Length claiming one |
500 | a document would not parse, a group is missing, a template could not be filled, or the lookup panicked |
503 | at RESCRIPTUM_MAX_CONNECTIONS — written promptly, then the connection closes |
Response headers
| Header | Value |
|---|---|
Content-Type | from the answer’s format — see the table below |
Content-Length | always set |
Connection | close |
WWW-Authenticate | Bearer, on 401 |
| Format | Content-Type |
|---|---|
toml, and every text format (ks, preseed, cfg, seed, ipxe) | text/plain; charset=utf-8 |
yaml, yml | text/yaml; charset=utf-8 |
json, ign | application/json |
xml, autoyast, unattend | application/xml; charset=utf-8 |
TOML is served as text/plain rather than application/toml because that is what the
Proxmox installer expects.
Request handling limits
| Body cap | 1 MB. An implausible Content-Length is refused from the header, before the body is read at all — rather than allocating for it and tripping a limit later |
| Header-read timeout | RESCRIPTUM_TIMEOUT_SECS, default 10 s |
| Whole-connection deadline | the same value. Both are needed: the header timeout stops at the end of the headers, so a client that promises a body and sends nothing would otherwise park a connection indefinitely |
| Concurrency | RESCRIPTUM_MAX_CONNECTIONS in flight; over that, a 503 and close rather than queueing |
| Authentication | only when RESCRIPTUM_ANSWER_TOKEN is set. Compared in constant time. Failures are logged and never rate-limited |
Authentication
Authorization: Bearer <RESCRIPTUM_ANSWER_TOKEN>
Proxmox sends this when its ISO was prepared with --answer-auth-token. Nothing else
can, which is why it is off by default. See Security.
The admin API
A separate listener, RESCRIPTUM_ADMIN_ADDR, over SQLite only. Full details on
its own page.
| Request | Does |
|---|---|
GET /machines, GET /groups | list identifiers |
GET /machines/{id}, GET /groups/{name}, GET /default | the stored document, as written |
PUT /machines/{id}, PUT /groups/{name}, PUT /default | store a document |
DELETE /machines/{id}, DELETE /groups/{name}, DELETE /default | remove one |
GET /resolve/{id} | the merged answer that machine would receive |
GET /check | current problems |
GET /health | liveness — no token, never blocked |
All document endpoints take ?format=<ext>, defaulting to toml.
| Code | When |
|---|---|
200 | done |
400 | malformed document, invalid identifier, or a non-UTF-8 body |
401 | missing or wrong token |
404 | no such document or endpoint; nothing resolves for that identifier |
409 | the write would have broken the answer set (rolled back), or a resolve that could not render |
413 | document over 256 KB |
429 | this address is blocked; Retry-After says for how long |
500 | the store could not be read or written |
Every admin response sets Connection: close. Successful GET /resolve also sets
X-Answer-Source, carrying the same description the log line uses.
Logging
One line per request, on stderr by default. RESCRIPTUM_LOG chooses what is kept and
RESCRIPTUM_LOG_FILE chooses where it goes — see logging.
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=example-rack bytes=431
Server-level lines carry - where the peer address would be. See
troubleshooting.
Formats and endpoint aliases
Formats and endpoint aliases
Three tables. The narrative version is in one document per operating system.
Document extensions
The allowlist of extensions rescriptum will pick up from a store. Anything else is
ignored — txt is deliberately not on the list, so a stray notes file next to your
answers never becomes a candidate.
| Extension | Family | Layering | Content-Type |
|---|---|---|---|
toml | TOML | structural merge | text/plain; charset=utf-8 |
yaml, yml | YAML | structural merge | text/yaml; charset=utf-8 |
json, ign | JSON | structural merge | application/json |
xml, autoyast, unattend | XML | structural merge, by element | application/xml; charset=utf-8 |
ks, cfg, preseed, seed, ipxe | text | concatenation in layer order | text/plain; charset=utf-8 |
The family is what the log line’s format= field reports, so ks and preseed both
appear as format=text. The extension is what an endpoint filters on, and what
check needs in order to pick the right validator.
Endpoint aliases
A path segment naming one of these restricts the answer to documents with the listed
extensions. Any segment of the path may name it, so /rhel/ks, /ks and
/provision/rhel/node.cfg all restrict to kickstart.
| Segment | Serves | Typical use |
|---|---|---|
proxmox, pve, toml | .toml | Proxmox VE |
debian, preseed | .preseed, .seed | Debian preseed |
rhel, centos, fedora, alma, rocky, kickstart, ks | .ks | kickstart |
ubuntu, autoinstall, cloudinit, nocloud, yaml, yml | .yaml, .yml | Ubuntu autoinstall, cloud-init |
flatcar, coreos, ignition, ign | .ign, .json | Ignition |
suse, opensuse, autoyast | .autoyast, .xml | AutoYaST |
windows, unattend | .unattend, .xml | Windows unattend.xml |
json | .json, .ign | |
xml | .xml | |
cfg | .cfg | |
ipxe | .ipxe |
A segment naming none of these constrains nothing, which is why /answer keeps
working exactly as it always has.
Two traps in this table
- Filtering is on the extension, not the family.
.ksand.preseedare both text documents; filtering by family would let a preseed answer/rhel/ks. seedis deliberately not an alias.s=http://server/seed/is an ordinary NoCloud seed URL, and it serves YAML. An alias has to be specific enough that nobody reaches it by accident. (The.seedextension still exists, and/debian/serves it.)
Control keys, per format
Stripped before the answer is sent.
| Format | Spelling |
|---|---|
| TOML | top-level extends = "base", members = […], [match] table |
| YAML | top-level extends:, members:, match: |
| JSON | top-level "extends", "members", "match" |
| XML | <answer-meta extends="base"><member>…</member><match k="v" /></answer-meta> |
| Text | # answer: extends <name> · # answer: member a, b · # answer: match k=v k2=v2 |
Text directives also accept // as the comment marker. match takes space-separated
key=pattern pairs; member a comma-separated list. Ordinary comments in a text document
are served — only # answer: lines are removed.
Merge semantics
| Structural formats | Text formats | |
|---|---|---|
| Maps / objects / elements | merge recursively | — |
| Scalars | higher layer replaces | — |
| Arrays / lists | replace, never append | — |
| Whole document | — | concatenated in layer order |
XML pairs siblings by element name plus a discriminating attribute — name, id, key,
alias, pass — and honours config:type="list". Declarations, doctypes, namespaces and
attributes survive a merge; original indentation and comment placement do not.
Validators check can call
| Format | Tool | Invoked as |
|---|---|---|
toml | proxmox-auto-install-assistant | validate-answer <file> |
xml, autoyast, unattend | xmllint | --noout <file> |
ks | ksvalidator | <file> |
| everything else | — | none exists |
A tool that is not on PATH is reported once as a note, never as a failure.
Command line
Command line
With no arguments, rescriptum runs the server. Everything else is a subcommand.
| Command | Does |
|---|---|
rescriptum | run the server |
rescriptum render <id> | print the answer that identifier would receive |
rescriptum render --body FILE | …for a captured request body |
rescriptum render --query Q | …for labels, e.g. "mac=aa:bb&serial=7ABC1" |
rescriptum check | render everything in the configured store and report what breaks |
rescriptum import <dir> | copy a directory of documents into the configured store |
rescriptum export <dir> | write the configured store out as a directory of documents |
rescriptum migrate [<dir>] | show what a flat answers directory would become |
rescriptum migrate --apply | move those documents into a directory each |
rescriptum config | show the configuration, and where each value comes from |
rescriptum config --json | the same, for a settings panel |
rescriptum config --value KEY | one value, for a script — never a credential |
rescriptum config set K=V … | edit the file RESCRIPTUM_CONFIG or RESCRIPTUM_ENV_FILE names |
rescriptum config unset KEY … | take a setting back out of it |
rescriptum --help | usage and the environment variables |
All of them read the same environment variables, including
RESCRIPTUM_CONFIG and
RESCRIPTUM_ENV_FILE — which are resolved first, so a
file that cannot be read stops any command that needs configuration. --help and
--version are answered before it is read, because they are what you reach for when
something is wrong. There are no global flags.
render
$ rescriptum render 98:fa:9b:50:d8:10
$ rescriptum render --query "serial=7ABC123&mac=98:fa:9b:50:d8:10"
$ rescriptum render --query "path=/rhel/ks&serial=7ABC123"
$ rescriptum render --body /var/log/rescriptum-captures/2026…-0000.body
| Form | Facts it supplies |
|---|---|
<id> | the identifier as a haystack, and nothing else — enough to match by name, not enough for a selector on serial |
--query "k=v&k2=v2" | those labels, percent-decoded. path= also yields file and segment, and constrains the format the way a real URL would |
--body FILE | the file verbatim: haystack, plus flattened JSON if it parses as JSON |
- The document goes to stdout; the
# format=… machine=… group=…line explaining how it was reached goes to stderr. Sorender … > answer.tomlgives you just the document. - Load-time problems are printed as
warning:lines first. - Exit 0 when something resolved, 1 when nothing applied (the server would have
returned
404) or rendering failed.
check
$ rescriptum check
Reports load-time problems, renders every machine document and every group member, names
the groups that select on a match block (which it cannot try without a real request),
and calls the installer’s own validator where one is on PATH.
Exit 0 when everything renders, 1 when anything failed — so it drops into CI as is. See validating.
import / export
$ RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db rescriptum import /srv/answers
$ RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db rescriptum export /tmp/backup
import reads a directory and writes into the configured store; export does the
reverse. The round trip is byte-identical, paths included. Neither runs check for you —
the output says to.
migrate
Answers used to be files at the top of the answers directory — 98fa9b50d810.toml beside
98fa9b50d810.preseed. They are now a directory each, and a document left flat is
reported and not served. This moves them:
$ rescriptum migrate
migrating /srv/answers
98fa9b50d810.toml -> 98fa9b50d810/proxmox.toml
98fa9b50d810.ipxe -> 98fa9b50d810/boot.ipxe
groups/rack-a.toml -> groups/rack-a/proxmox.toml
default.toml -> default/proxmox.toml
4 document(s) to move — nothing has been changed. Re-run with --apply.
It shows by default and moves only when told to. The answers directory is what a rack installs from; typing the command to find out what it would do must not rearrange it.
--apply performs the moves, each a rename within the same directory, so no document is
ever rewritten. If any destination is already taken, nothing moves at all — including
the documents that could have — and the collisions are named: a half-migrated directory is
the state nobody can reason about. It takes a directory as an argument, defaulting to
RESCRIPTUM_ANSWERS_DIR, and running it on an already-migrated directory says there is
nothing to move.
config
The configuration is environment variables, and on a deployment that reads them from a file — a packaged install, mainly — this is how to see and change them without opening an editor. It is also what the DSM application runs underneath.
$ rescriptum config
env file: /var/packages/rescriptum/etc/rescriptum.env
RESCRIPTUM_STORE files default
RESCRIPTUM_ANSWERS_DIR /volume1/netboot/answers file
RESCRIPTUM_LISTEN_ADDR 0.0.0.0:9000 environment
RESCRIPTUM_ADMIN_TOKEN (set) file
The third column is the point. The files supply defaults and the real environment
wins, so a value marked environment cannot be changed by editing a file — and config set says so rather than letting you write something the running server will ignore. With
a TOML file the column reads toml file, and naming both files prints both paths plus the
order they win in.
config set writes the TOML file when both are named, because it is the one the server
reads first: writing the other would be a change that silently does nothing.
A credential is never printed, by any form of this command. A token shows as (set) or
(not set); --value refuses outright.
$ rescriptum config set RESCRIPTUM_LOG=problems RESCRIPTUM_CAPTURE_DIR=/srv/captures
wrote /var/packages/rescriptum/etc/rescriptum.env
Writing keeps the file as it is otherwise: comments stay, a setting is replaced where it
stands, and one that is commented out is uncommented in place rather than appended
below — which matters when the comment above it is the only documentation the file has. In
a TOML file the same care applies to the document: the value is replaced where it stands,
its trailing comment survives, and config unset empties the value rather than deleting
the line, so the paragraph explaining the setting stays where it was.
Two refusals are deliberate:
- A change that would leave a server unable to start is refused, whole, before anything is written. Turning on the admin API without a token, or with a token shorter than 16 characters, gets the reason instead of a broken next boot.
- A misspelled variable is refused. Written, it would be read back as a stranger and warned about at the next start, by which time nobody connects the two.
Unlike every other subcommand, this one works when the configuration is too broken to start a server — a file that will not parse, a token one character short. That is the state people run it to get out of.
media
Boot media: the installer images this server holds. Every one of these needs
RESCRIPTUM_MEDIA_DIR; without it they say so and exit 1. See
Serving boot media.
$ rescriptum media list # what is held: family, architecture, version, digest
$ rescriptum media add FILE [--sha256 D] # register one already in the directory
$ rescriptum media add URL --sha256 D # fetch one into it, then register it
$ rescriptum media check # re-verify every recorded digest
$ rescriptum media ipxe ID # print the .ipxe answer that boots one image
$ rescriptum media prepare ID [--url URL] # a Proxmox image with its answer URL inside
$ rescriptum media export ID FILE # materialise a prepared entry, for a stick
media add takes a file already inside the media directory — nothing is downloaded
and nothing is copied. It hashes it with progress, probes it, and writes a .media
sidecar beside it. --sha256 is checked before anything is recorded: a mismatch writes
nothing and exits 1.
media check’s exit status is a contract, like check’s. deploy.sh keys on it.
media ipxe prints to stdout and puts everything else on stderr, so
rescriptum media ipxe pve-8.4 > groups/rack-a/boot.ipxe produces a usable answer document —
which is all it is. It prints a script; it does not install one.
boot
The netboot half: TFTP’s loaders, the generated DHCP configuration, and the two scripts a machine executes. See Netbooting a machine.
$ rescriptum boot dhcp-snippet [--format F] [--one-loader]
$ rescriptum boot check # are the loaders a snippet names actually on disk?
$ rescriptum boot bootstrap # print the stage-two script
$ rescriptum boot menu # print the built-in menu
--format is dnsmasq (the default), isc, kea, powershell, pfsense or
mikrotik. The snippet goes to stdout and warnings to stderr, so
boot dhcp-snippet > dhcpd.conf produces a file that can be included as-is.
boot check’s exit status is a contract, like check’s. What it catches is the least
diagnosable failure in the chain: a snippet naming a loader that is not on disk fails
silently at the ROM, with nothing on any console. It also warns when the media listener
has moved off the port shipped loaders embed.
boot bootstrap and boot menu print what a machine will execute, for the same reason
render prints an answer: everything a rack runs should be readable by a human first.
Exit statuses
| Status | Means |
|---|---|
0 | success |
1 | the command failed — nothing resolved, a document would not parse, the store could not be opened |
config is the one with a second meaning: 0 says the configuration is one the server
would start on, 1 that it is not — or that a write was refused. That makes it usable
from a script the way check is.
The server itself exits 0 on SIGTERM or Ctrl-C, and 1 if it cannot bind or cannot
open the store.