v1.18.0
This release fixes a few security issues, makes collection --force safer, and cleans up some smaller bugs and quirks.
The biggest change is how Resterm handles secrets. resterm run now masks them throughout its output, including values that weren't referenced through templates. History also masks secrets captured later in a session.
There's also a new --response flag for including responses in JSON reports, a skill for coding agents, and a change to OAuth handling so headless runs can no longer open a browser.
I added the skill because I realized that most folks now use local LLMs or frontier models like Claude or Codex, so it should help your LLM work with Resterm.
Secret masking in resterm run
In previous versions, Resterm masked secrets referenced through templates like {{api.key}}. But if you put the same value directly in a URL, it could still appear in the report:
# @file-secret api.key s3cr3t-token-value-123
### Search
# @name search
GET http://localhost:3000/search?key=s3cr3t-token-value-123The output looked like this:
PASS GET search [200 OK in 550.625µs]
Source Target: http://localhost:3000/search?key=s3cr3t-token-value-123
Effective Target: http://localhost:3000/search?key=•••
In 1.18.0:
PASS GET search [200 OK in 447.458µs]
The target lines are only printed when they differ. Once both values are masked, there's no difference to show.
- Secrets are masked with
•••in text, JSON, and JUnit reports, stream transcripts, trace files, and errors. This also covers JSONtargetfields, JUnit test names, and the request list printed when a file contains multiple requests. - Masking uses all secrets known by the end of the run. If a later request captures a token as a secret, that value is also masked in earlier results.
- Secrets declared with fewer than 4 characters produce a warning, since masking a short value might also replace unrelated text:
Warnings:
secret "pin" is shorter than 4 characters, so reports may also mask the same word in unrelated text
JSON reports include this under warnings. The warning doesn't change the exit code.
Include responses in JSON reports
You can now use --response to include response data in the JSON report. It adds a response object to request results, including individual workflow, for-each, and compare steps.
resterm run --all --format json --response api.httpExample output:
"response": {
"headers": {
"Content-Type": ["application/json"],
"Set-Cookie": ["•••"]
},
"body": "{\"token\":\"•••\",\"user\":\"ada\"}",
"bodySize": 47,
"captures": [
{"scope": "file", "name": "auth.token", "value": "•••", "secret": true}
]
}Responses are masked before being written to the report.
- Secret values,
-secretcaptures,Set-Cookie, and sensitive headers such asAuthorizationandX-API-Keyare replaced with•••. - Gzip bodies are decoded before masking. If Resterm can't decode a body, such as with
bror invalid gzip data, thebodyfield is omitted. Empty bodies are kept as"body": "", along with their headers. - Bodies that aren't valid UTF-8 are base64-encoded and include
"bodyEncoding": "base64". - Masked bodies larger than 1 MiB are truncated to the first 1 MiB, with
"bodyTruncated": true. - Skipped requests, profile runs, and requests without a response don't include a
responsefield.
The flag only works with JSON output. Using it with another format exits with code 2:
error[unknown]: run: --response can only be combined with --format json
If you're using Resterm as a Go package, the equivalent option is Response.Enabled.
History
History now uses ••• for masked values, matching the reports. Previously, it used *** and only masked secrets known when an entry was saved. That meant a token captured later could still appear in an earlier history entry.
Here's an example where preview receives a token before login captures it as a secret:
### Preview
# @name preview
POST http://localhost:3000/login
### Login
# @name login
# @capture file-secret auth.token {{response.json.token}}
POST http://localhost:3000/loginPreviously, resterm history export could produce:
{"requestName":"login","bodySnippet":"{\"token\":\"***\",\"user\":\"ada\"}"}
{"requestName":"preview","bodySnippet":"{\"token\":\"s3cr3t-token-value-123\",\"user\":\"ada\"}"}
In 1.18.0, both entries are masked:
{"requestName":"login","bodySnippet":"{\"token\":\"•••\",\"user\":\"ada\"}"}
{"requestName":"preview","bodySnippet":"{\"token\":\"•••\",\"user\":\"ada\"}"}
This works in both the TUI and resterm run --history. When Resterm learns a new secret, it also masks that value in earlier entries from the same session or run.
See history.
A skill for coding agents
Resterm doesn't have an MCP server or built-in AI features, but it now comes with a skill for coding agents such as Claude Code and Codex. The skill explains how to write Resterm request files, execute them with resterm run, and use JSON reports and exit codes to inspect the results. It also covers a few rules: don't launch the TUI, don't put secrets in request files, and don't try to recover masked values.
You can print the skill or install it directly:
# Print it
resterm skill
# Install it for Claude Code or Codex, for all projects
resterm skill --agent claude
resterm skill --agent codex
# Or for one project
resterm skill --dir .claude/skills--agent claudeinstalls to~/.claude/skills/resterm, or$CLAUDE_CONFIG_DIR/skills/restermif set.--agent codexinstalls to~/.agents/skills/resterm. Gemini CLI and Copilot CLI can read skills from that location too.- The skill is bundled with the Resterm binary, so it always matches the installed version. Run the install command again after upgrading.
- Installation only replaces an existing directory if it already contains the Resterm skill. Other directories named
resterm, and symlinks, are left untouched. The command exits with 1 in those cases.
You can also install the skill without Resterm:
npx skills add unkn0wn-root/resterm@v1.18.0See skill.
Safer collection --force
There was a rather nasty issue with collection --force. When you passed --force, Resterm deleted whatever existed at --out before writing the new output. It didn't check whether the directory belonged to Resterm.
For example, suppose you had a notes directory containing plan.md and todo.txt:
$ resterm collection export --workspace ws --out notes --force
Exported 2 files to notes
The export would succeed, but your original files would be gone. Using --out . could do the same to an entire project directory.
That's fixed in this release. Resterm now refuses to replace directories it doesn't recognize as its own bundles:
error[unknown]: collection export: notes exists and is not a resterm bundle. Remove it or choose another path
exportandunpackonly replace directories containing a bundlemanifest.json.packonly replaces files. Symlinks are refused.- Resterm writes the new output beside the old one before swapping them. If something goes wrong, the old output is preserved, or the error tells you where the old output was kept.
- Interrupting
export,pack, orunpackwithCtrl+CorSIGTERMleaves either the original output or the complete replacement. The command exits with 130. - Interrupting
importkeeps the files already written and reports how many were imported. You can run it again with--forceto finish.
See collections.
OAuth
resterm run and the headless package no longer open a browser.
Previously, if a cached authorization_code token couldn't be refreshed, Resterm tried opening auth_url through the system's default opener. This could happen even when running in CI.
Now the request fails instead:
error[auth]: oauth token refresh failed: 400 Bad Request
fetch oauth token
╰─> cannot fall back to browser login
The exit code is 23. Browser-based login is still available in the TUI.
Two smaller fixes:
auth_urlmust usehttp://orhttps://and include a host. Previously, values such asfile:///etc/hostscould be passed to the system opener. Invalid URLs now fail withauth_url must use http or httpsorauth_url must contain a host.- If a token refresh response doesn't contain a new
refresh_token, Resterm keeps the existing one, as expected by RFC 6749. In older versions, the old token was discarded. With--persist-auth, this could cause the next refresh to fail withheadless oauth authorization_code requires a cached or refreshable token.
See authentication.
Cleaner error messages
Some errors contain several problems, and Resterm wasn't formatting them properly. Line breaks could end up inside the error header, sometimes printed literally as \n.
For example, running resterm collection without a subcommand produced:
error[unknown]: Usage: resterm collection <export|import|pack|unpack> [flags]\n\nSubcommands:\n export --workspace <dir> ...
Now the usage information goes to stderr, followed by a single-line error:
Usage: resterm collection <export|import|pack|unpack> [flags]
...
error[unknown]: collection: subcommand is required
The same applies to resterm history.
Joined errors also have cleaner headers. Individual problems still appear in the error tree, but the header puts them on one line, separated by ; . This fixes formatting for invalid @ssh and @k8s profiles, SSH tunnel errors, and rejected @auth directives. Invalid @mock options are reported individually, and rejected @auth directives no longer show literal \n characters in the TUI response pane.
TUI fixes
- Failed saves no longer destroy file contents. In 1.17.2, saving to a full disk could truncate the file before the write failed, leaving it empty or incomplete. The file watcher could then treat that as an external change and reload the damaged contents into the editor, where undo couldn't recover the original text. Resterm now writes without truncating the file first and restores its original contents if the write fails. The editor keeps your text and marks the file as unsaved. Symlinks, hard links, ownership, and file permissions are preserved.
- Canceled requests no longer overwrite newer responses. If a canceled request returned late, its response could replace the result of a newer request. That no longer happens.
- Cancellation is handled properly for workflows, compares, and profiles. Sending another request now waits for the canceled run to stop, so results don't get mixed together. Loading an entry from history while a request is running no longer stops you from canceling it.
Documentation fixes
- The workflow example now includes
# @namefor each request. Previously, it only used###headings, which don't actually name requests. As a result,using=couldn't find them. - The
@ifand@caseexamples now use request names. The docs also clarify thatrun=refers to the name defined by@name. - The docs clarify that
capture.strictapplies to template captures such as{{response.json.token}}. RTS captures still store an empty string, so use@assertif you need to check them.
Upgrading from 1.17.2
Most existing request files should work without changes. If you have CI jobs or scripts that parse Resterm's output, there are a few differences to be aware of:
- History now uses
•••instead of***when masking secrets, including inhistory export. - Secrets written directly into requests are masked in text, JSON, and JUnit reports, as well as request lists.
Source TargetandEffective Targetare omitted from text reports if they're identical after masking. resterm runno longer opens a browser. If anauthorization_codetoken can't be refreshed, the request fails and the run exits with 23. Use the TUI for browser-based login, or choose a grant that doesn't require a browser.collection export --forceandunpack --forcerefuse directories that aren't Resterm bundles.pack --forcealso refuses directories. These cases exit with 1 without modifying the existing output.- Secrets declared with fewer than 4 characters produce a warning in reports.
resterm run --historynow writes history when the run finishes.auth_urlmust usehttp://orhttps://.resterm skillis now a command. If you have a request file namedskill, run it withresterm ./skill.resterm collectionandresterm historynow print usage information to stderr, followed by a one-line error.- Saving a file in the TUI now requires read access to that file.