Changelog
Notable changes to Varlatch, newest first. Varlatch is before 1.0: a minor
release may change the /v1 API, the CLI, configuration, or the database
schema, and its entry says what to do. Only the latest release receives
fixes.
0.21.0 (2026-11-11)
Section titled “0.21.0 (2026-11-11)”Backups
Section titled “Backups”- A failed upload or remote check says why. “Remote archive retrieval
failed” and “Destination upload failed” now give the HTTP status and,
when Varlatch recognizes it, the storage error code (for example
S3 error InvalidAccessKeyId, HTTP 403) or the network error, with what to check: the key, its access to the bucket, the archive, or the host clock. Codes Varlatch does not recognize, the storage provider’s message and the key are never shown.
varlatch scan --find-filesfinds copies of secrets on disk. It lists the files under a directory (the current one by default) whose names look like copies of secrets:.env,.env.*except.env.exampleand.env.schema,*.pemand*.key. For each it says whether Git ignores it, and it suggestsvarlatch importto move a dotenv file in. Discovery uses file names and metadata, without reading the contents of the files it lists, does not follow symbolic links, skips.gitandnode_modules, and needs no project, sign-in or server. The ignore check runsgit check-ignore --no-index, which reads Git’s configuration and ignore rules; a setup it does not support (an include in the configuration, a linked or hard-linked file Git would open, another object store) reports the state as unknown, and Git’s own messages are never shown. Changing those files while the command runs is outside what it guarantees. It exits 1 when it finds a file and 2 when part of the tree was not searched. The secret scanning reference also shows how to check a shell history file for values withvarlatch scan <file>. See Finding copies of secrets.
Machine identities
Section titled “Machine identities”- See what a machine can reach. Open a machine in Access, then
Machines, or run
varlatch identity reach <id or name>(--jsonfor the whole document), to see its state (live, disabled, retired), its live credentials, and per project and environment the actions its grants allow, directly or through groups, roles and teams. Value reads covered by a tailnet requirement say so as a condition, such asneeds tailnet example.ts.net: tag:prod: requirements are listed, not checked against any device, so a condition is never shown as a denial. A derived environment shows what its root’s grants and requirements give it. For a broker it notes that agent runs use Capabilities, which are not its grants. The reach is computed when asked and never stored; it changes no access. It needs bothidentity.readandpolicy.read, since it shows grants and requirements, and refuses a person: for a member, see the exposure report. API:GET /v1/organizations/{org}/identities/{identity}/reach, capabilityidentity.reach. - See which machine credentials may no longer be needed.
varlatch security report(--jsonfor the whole document) lists the organization’s machine credentials that were never used, have not been used for 30 days, or never expire, grouped by reason, each with its identity, when it was last used, its expiry and who issued it, and a count per reason. Revoked, expired and used-up credentials are not listed, nor are people’s credentials or the short-lived credentials of OIDC sign-ins and agent-safe runs. A credential of a machine suspended by the organization’s deletion is listed with that state. It only reports: nothing is revoked or retired, and it changes no access. It needsidentity.manage, asvarlatch credential listdoes. API:GET /v1/organizations/{org}/security/credential-review, capabilitysecurity.credential-review. - The CLI warns when the credential in
VARLATCH_TOKENis about to expire. Until now it warned only about the credentialvarlatch loginstored, and said nothing forVARLATCH_TOKEN, where machine credentials issued with an expiry live, until requests started failing. Now every/v1response to a request authenticated by a machine credential that expires within 7 days, whatever its total lifetime, errors included, carries aVarlatch-Credential-Expiresheader with the expiry (ISO 8601, UTC). Credentials renewed by design never get it: a person’s dashboard sessions and CLI sign-ins, OIDC exchanges, and agent-safe runs. The CLI turns it into one warning on stderr per command, beforevarlatch runstarts its command, and never writes it to stdout, so the output scripts parse,varlatch env listincluded, is unchanged. It does not warn inside an agent-safe run. In the dashboard, Access → Machines marks, by the same rule, a machine credential that expires within 7 days or has expired. Credentials without an expiry or expiring later behave exactly as before. The tailnet browser endpoint exposes the header to the pages it allows.
Security controls
Section titled “Security controls”- Optional security controls per organization, all off. Four
controls:
requirement-protection(editing or revoking a tailnet requirement),admin-step-up(administrative changes),passkey-verification(people’s access with a verified sign-in by a trusted passkey) andcredential-lifetime(a default and a maximum lifetime for machine credentials). Each isoff,reportorenforce. They start off on new and upgraded installations alike, and a control that is off adds no access restriction.reportrefuses and changes nothing: it counts what the control would have done.enforceis not offered yet: the server refuses it withCONTROL_MODE_UNAVAILABLE. Organization admins manage them on the organization’s Security page, or withvarlatch security controlsandvarlatch security set <control> <off|report> [--default-ttl <s> --max-ttl <s>]. Reading needsorganization.read; changing needsorganization.manageand a person’s sign-in, never a machine credential. Every change is recorded assecurity_control.changed. See Security controls and approvals. API:GET /v1/organizations/{org}/security/controlsandPATCH /v1/organizations/{org}/security/controls/{control}, capabilitysecurity.controls. - Report mode counts what each control would have done. With a
control at
report, nothing is refused and nothing issued changes; instead Varlatch counts, per hour, each time the control would have acted:requirement-protectioneach tailnet requirement edit or revocation,admin-step-upeach administrative change on its list (machine identities and credentials, grants, roles, groups, teams, admins, member removal, webhooks, connections, the GitHub App, project and organization deletion,.envexport),passkey-verificationeach request by a person whose sign-in was not made with a trusted passkey that verified them (machine credentials never), including each organization in the dashboard’s realtime token, andcredential-lifetimeeach machine credential issued without a lifetime (would get the default) or with one above the maximum (would be refused). A change is counted only when it was saved and changed something, an access only when it was allowed and succeeded. Access counts are best-effort: made after the response, never slowing a request, and dropped when they cannot be made within about a second or the server stops or enters maintenance first. Counts are kept per control version, operation, person or machine, reason and hour, with fixed fields only: never a request body, URL, token, hash or value. About 10 minutes after each hour, onesecurity_control.would_blockaudit event per counter summarizes it; counters are deleted 35 days after their summary.GET /v1/organizations/{org}/security/control-report?days=30returns the live current hour and the last 30 days per control, operation, reason and actor, fororganization.manage(capabilitysecurity.report). See What report mode counts. - Turning a control down needs your approval with a trusted passkey.
A change that moves a control towards off, makes a lifetime longer or
clears the lifetimes, even alongside a tightening, is answered with
STEP_UP_REQUIREDand an approval address. You open it in a browser where you are signed in, check the server’s summary, and approve with a trusted passkey and its PIN, fingerprint or face. The approval covers that exact change, the version it expects and the sign-in that asked, is seen and approved only by you, is used once, and expires 2 minutes (dashboard) or 10 minutes (CLI) after it was asked for. Tightening needs none. The dashboard shows the approval in a dialog; the CLI prints the address and waits, then sends the change again with theVarlatch-Approvalheader. In assisted mode the CLI does not wait: it prints the address and the command to run again with--approval <id>, and exits 75, so a coding agent stops and you approve. New exit status 130 when Ctrl-C stops the wait (the approval is cancelled). New error codesSTEP_UP_REQUIRED,APPROVAL_INVALIDandCONTROL_MODE_UNAVAILABLE; new routes under/v1/me/approvals. - Removing a trusted passkey will need an approval while you belong to
an organization with any control at
enforce; untilenforceis offered, removing passkeys works as before. - The Security page. Each organization has a Security page next to
Settings (
/o/<org>/security) for deciding which controls to turn on. It shows each control’s mode, whatreportcounts and whatenforcewould change, the counts of the last 30 days per operation, reason and person or machine (the current hour live), and the mode switch, with the lifetimes forcredential-lifetime;enforceis shown as coming in a later release. Below it: the admins’ approval keys (how many trusted and unconfirmed passkeys each has, by name), the machine credentials to review together with service credentials that expire within 7 days or have expired (linking to Access, Machines), the number of tailnet requirements (linking to their editor), and a short checklist that links each item to its docs page. Each part shows when your access allows it (a grant works as well as the admin role), and the page never asks for what you cannot read; a member sees the controls and a note naming what the rest needs. Settings links to the page.varlatch security status(--jsonfor one document) prints the same; a part you cannot read is named, not shown, and it exits 0. New API:GET /v1/organizations/{org}/security/approval-keys(per admin: trusted and unconfirmed passkeys with names and dates, never key material or ids; needsorganization.manage; capabilitysecurity.approval-keys) andGET /v1/organizations/{org}/me/permissions(your role and the organization-level actions you hold there, readable by every member; capabilityorganizations.permissions). The credential review (GET .../security/credential-reviewandvarlatch security report) gainsexpiry: the service credentials that expire within 7 days or have expired, by the rule of theVarlatch-Credential-Expiresheader; the text output ofsecurity reportnow starts with two lines, the use and expiry policy and the expiry warnings. See The Security page. - The host operator’s way back.
varlatch admin security reset --org <slug> (--control <name>... | --all)(oradmin security resetin thevarlatchdcontainer) sets controls back to off without anyone’s passkey, closes their open approvals, and recordssecurity_control.reset. It never touches deletion, suspended identities, passkeys or grants.
Passkeys
Section titled “Passkeys”- Each passkey is trusted or unconfirmed. The account’s Security page
says which, and why. Passkeys created from a link (setting up the
installation, an invitation,
varlatch admin reenroll,admin recover) are trusted, and so is every passkey that exists when you upgrade. A passkey added in the dashboard while signed in starts unconfirmed: a stolen session could add one, so it does not count as yours until you confirm it. Signing in, approving a CLI sign-in, and adding and removing passkeys work as before, with any passkey. Only a trusted passkey can approve a change that needs your approval (see Security controls below). See Your passkeys. - Confirm a new passkey with a trusted one. On the Security page,
Confirm with another passkey asks for one of your trusted passkeys
with its PIN, fingerprint or face, and then trusts the new one. The
confirmation is tied to that passkey and to your browser session, works
once, lasts 5 minutes, and is limited to 10 starts in 10 minutes. It is
recorded in the audit log as “Passkey confirmed”. A person with no trusted
passkey gets one through a link from whoever runs the server. API:
GET /v1/me/passkeys,POST /v1/me/passkeys/{passkey}/confirmation/optionsandPOST /v1/me/passkeys/{passkey}/confirmation, capabilityaccount.passkeys. - Sign-ins record how you verified. Each sign-in, and each CLI sign-in approved with a code, now records which passkey was used and whether it asked for your PIN, fingerprint or face. The CLI credential from the browser sign-in hands over keeps a link to the browser session it came from. Sessions and credentials from before the upgrade have no such record. This changes no access.
- The dashboard removes passkeys through Varlatch.
DELETE /v1/me/passkeys/{passkey}removes one of your own passkeys, as Better Auth’s/auth/passkey/delete-passkeydid; that endpoint is turned off and answers 404. The dashboard still never removes your last passkey. - A passkey that is already registered is refused. Registering a
credential that the server already knows, for anyone, now fails with a
400, as the WebAuthn standard asks, also when two registrations of it
arrive at once. If an installation already has such duplicates, the
upgrade keeps the oldest passkey of each credential and moves the later
copies to a quarantine table,
passkey_quarantine. A quarantined passkey can no longer sign in. When the passkey that was kept belongs to another person, the person whose passkey was quarantined loses it, and signing in with that authenticator signs in as the other person if it has the same public key. The migration job’s output (with Compose,docker compose logs varlatch-migrate) lists every quarantined passkey with its person and says whose passkey was kept; the table also records it (kept_passkey_id). Whoever runs the server restores access withvarlatch admin reenroll --identity <identity id>, which sends a link that enrolls a new trusted passkey. See Passkeys that shared a credential. - Security fix: a passkey’s signature counter can no longer go back. Each use of a passkey that counts its uses must show a higher count than the last, which lets the server notice and refuse a copied authenticator. Two sign-ins with the same passkey at the same moment could both pass, and the second to finish could store the lower count, which weakened that check. Now the one with the lower count fails as a sign-in with a stale count does, without a session, for browser sign-ins, CLI sign-in approvals and passkey confirmations alike. Passkeys that report no count (always 0, as most passkeys synced by Apple or Google do) work as before, also at the same moment. Migration 40 adds the database check.
Upgrading
Section titled “Upgrading”Database schema: migration 39 adds passkey provenance (every passkey that
exists at the upgrade is trusted), sign-in evidence for sessions, CLI
credentials and device sign-ins, passkey_quarantine and a unique index on
the passkey credential id. If two passkeys shared a credential id, the
oldest stays and the others move to passkey_quarantine; the migrate step
lists each one with the command that restores access (see
Passkeys that shared a credential).
Migration 40 adds a trigger that keeps a passkey’s signature counter from
going backwards. Migration 41 adds the security control and approval
tables, and migration 42 the report counters (nothing is counted while
every control is off). Every varlatchd process now runs a loop that writes
the hourly report summaries.
Every security control starts off, on existing and new installations, and
enforce is not available yet: a control that is off adds no access
restriction. What changes regardless:
- every passkey sign-in, device sign-in approval and CLI browser handoff records whether a PIN or biometric was used and which passkey signed in, and a CLI credential from the browser handoff now carries its browser session;
- the dashboard removes passkeys through a Varlatch route (Better Auth’s own delete endpoint is turned off), and registering a passkey that is already registered is refused;
- responses to a machine credential of kind
servicein its last 7 days carryVarlatch-Credential-Expires, and the CLI warns about it on stderr; OIDC, agent-run and people’s credentials never get it.
The new server features (varlatch security, identity reach and the
Security page) need this release on the server: upgrade the server before
the CLIs that use them. varlatch scan --find-files runs offline and works
with any server.
Backups: archives of 0.20.0 and earlier still restore into this release. From 0.19.1 or earlier, read the 0.20.0 notes below as well.
0.20.0 (2026-10-30)
Section titled “0.20.0 (2026-10-30)”Integrations
Section titled “Integrations”- Pausing or revoking an integration stops a push that is already running. Until now a pause only stopped new pushes: a push in progress kept writing, deleting and redeploying, and the audit log showed it after “Integration paused”. Now the push sends no further requests and records nothing more once the pause or revocation commits. A request it had already sent can still reach the platform. Pausing a paused integration changes nothing, and pausing a disabled one is refused: resume or re-point it instead.
- The dashboard shows when a push is running, and when a pause has taken hold. Every push now has a deadline (240 seconds) after which it starts no further request; a push that reaches it stops and continues on the next pass, without counting as a failure. A repair larger than one push’s budget (for example a large first sync to GitHub, which cannot read values back) now resumes where it stopped instead of starting over, and a value changed meanwhile is still pushed. An integration reads “Pushing” while a push can still send, and “Pausing” after a pause until the push in progress can start no other request. The pause button waits for that (up to 30 seconds). Revoking a Connection or removing a GitHub App now also stops a push in progress.
- A redeploy that is owed is no longer lost. When a push changes values on a Coolify destination with redeploy turned on, the redeploy is recorded as owed with those changes, and any later push sends it, even one with nothing to write, until Coolify accepts it. A push cut off after its writes, or a redeploy Coolify refused, is therefore tried again instead of leaving the running application on old values. After a crash at the wrong moment the application may be redeployed twice. Changing the destination or turning redeploy off cancels an owed redeploy.
- Drift is named before it is put back. When the periodic repair of a
Convex or Coolify integration finds a value that differs from what
Varlatch last wrote (changed or removed on the platform, or a deleted
name the platform has again), it records
sync.drift_detectedbefore putting it back: destination and item names only, never a value. The integration card shows the last drift, the item’s history and the audit log show the event, and audit webhooks can deliver it. It is usually a change made on the platform, but the event says what was observed, not who did it. Putting drift back counts as a change, so an integration with redeploy turned on redeploys. GitHub Actions cannot read values back, so its integrations report no drift. A Convex value that is not a string now reads as unknown instead of empty.
Contracts
Section titled “Contracts”- Rotation hints. A Contract item can say how its credential is
rotated, in a sentence or two:
rotationHintin a JSON Contract, or@rotationHint=...in.env.schema. The dashboard shows it in the rotate dialog, the item panel and the Contract page, and the Contract editor of a project managed in the dashboard can edit it, along with the default rotation grace window.varlatch values rotateprints it on stderr before it asks for the new value, and again after. It never appears in generated types. A hint is one paragraph of 1 to 1024 characters: tabs, line breaks, control characters and bidirectional formatting characters are refused rather than removed. Every Member can read the Contract, so a hint must never contain a secret. @rotationGrace=<seconds>in.env.schemasets an item’s default rotation grace window, which until now only a JSON Contract could set. Each rotation decorator may appear once per item.
Projects
Section titled “Projects”- A Project can be deleted. Deleting one, which needs
project.manage, tombstones it and every environment and erases every version of its values from the live database, those of environments deleted earlier included. It also revokes the Capabilities of its environments and its integrations (a push in progress stops, and the destination can be used by another integration), and removes Teams’ links to it. Contract Revisions and the audit log stay. The slug can be used again, by a new Project that inherits no Grant of the old one. Not reached by the deletion: PostgreSQL’s old row versions and write-ahead log for a while, backups taken before, values already pushed to platforms, and copies people made. - Writes check what they write into, inside their own transaction. Every write that creates values or access now locks its Organization, Project and environment in a fixed order and checks that they still exist before it changes anything. A value can no longer land in an environment deleted a moment earlier, and a deletion waits for writes already under way.
Organizations
Section titled “Organizations”- An Organization can be deleted, after a waiting period. An admin schedules the deletion to run 1 to 30 days later (7 by default); when the Organization has more than one admin, another admin must approve it, and any admin can cancel it until it runs. While it waits, the Organization is frozen: nothing can be changed (the refusal says from when the deletion may run), but people can still read and make a last copy. Integrations are paused, webhooks and machine identities suspended, and pending invitations revoked; cancelling brings back the webhooks and machine identities. When it runs, every membership ends (people keep their accounts), machine identities are retired, integrations, Connections and webhooks are revoked, the GitHub App’s registration is removed (the App itself stays on GitHub), every value is erased and the Organization’s key is destroyed, so nothing it protected can be read from the live database any more. The people who could read the audit log at that moment can still export it; an Installation Admin can see that the Organization existed and remove such a reader, but not add one or read the events. Not reached by the deletion: PostgreSQL’s old row versions and write-ahead log for a while, backups taken before, values pushed to platforms, and copies people made.
- The dashboard deletes Projects and Organizations. A Project’s menu has Delete project, confirmed by typing its slug. Settings has a Delete organization section: an admin chooses the waiting period and types the slug; while the request is open it shows who scheduled it, when it may run and whether it waits for approval, with Approve for another admin and Cancel for any admin. Every page of a frozen Organization shows a banner saying from when it may be deleted.
Members
Section titled “Members”- An Organization Admin can remove a person, and a member can leave.
Until now nothing removed a membership, so an admin could not take
someone’s access away. Removal ends the membership and, in the same
step, revokes the person’s direct Grants, removes them from every group
and team, and revokes the invitations they issued that are still
pending. Access now needs a live membership: a Grant or group link that
still names a removed person gives them nothing, and they can be given
none. Their sign-in and credentials keep working, but from their next
request they reach nothing in the Organization. Removing someone needs
the built-in admin role; a Grant such as
organization.manageis not enough. The last admin cannot be removed, leave, or be made a member. - An admin can make a member an admin, and back. Making an admin a member removes the built-in admin role only: Grants given to them directly or through groups stay.
- Removal lists what to review. It changes nothing that belongs to the Organization, and says what the person set up that may keep working without them: integrations and Connections they created or changed, webhooks, OIDC bindings, machine credentials whose token they were shown (with each one’s state) and their personal environments. Removal also records, for a later report, what the person could reach at that moment and under which Tailnet Requirements.
- The identity listing keeps former members, without a role, and says when they left.
- In the dashboard, Access, then Members: an admin’s menu for each person has Make admin, Make member, and Remove from organization, and your own row has Leave organization. The remove and leave dialog lists what to review before anything changes; the Make member dialog lists the Grants the person keeps, directly or through groups and teams, so you can revoke what should go. Former members are listed apart. A person who is removed while signed in loses the organization in their open dashboard at once: its cached data and revealed values are dropped and its pages closed.
- What a person received, and what is still in use. The exposure report
of a current or former member separates three kinds of facts: what the
audit log shows they received (each version of each value, the
environments and ways they received it: revealed, delivered to a process
such as
varlatch run, read for an export, or written by them), what they could reach when they were removed (with the Tailnet Requirements it was conditional on), and what they set up. Each version says whether Varlatch still serves it, as the current version or a retiring one until its deadline, and shows the Contract’s rotation hint. Replacing a value in Varlatch does not revoke the old credential at its provider, so an operator confirms that separately, version by version. Reading the report needsaudit.read. The audit history recorded before this release is indexed in the background after the upgrade; until it is done, the report names the period it does not cover yet. In the dashboard, anyone who may read the audit log opens it from a person’s menu, from Former members, or from an audit event about the person, as a checklist: Rotate for each version still served, Revoked at provider to record the confirmation, and Revoke for a machine credential that still works.
varlatch member list [--json]lists the people in the organization, former members included.varlatch member remove <member> --confirm <member>removes someone and prints what to review,varlatch member leave --confirm <organization>leaves, andvarlatch member role <member> admin|memberchanges a role. A member is named by identity ID or by name. Removing and leaving always need--confirm; in assisted mode the refusal gives the human’s command, as forvalues delete.varlatch member exposure <member> [--json]prints a current or former member’s exposure report (with--json, one record per line); it is read-only and never shows a value.varlatch values listshows rotations in progress. With--json, each item hasrotating:truewhile a dual-phase rotation keeps its previous value working. Without--json, the list is unchanged, and stderr names each item that is rotating, with the command that completes it in the environment where the rotation lives (the parent, for an inherited item). The command still never fetches a value.varlatch project delete <slug> --confirm <slug>deletes a Project and says what it erased and what deletion does not reach.varlatch org delete <slug> --confirm <slug> [--after <days>]schedules an Organization’s deletion;--approve <request-id>and--cancel <request-id>approve or cancel it, andvarlatch org deletion <slug> [--json]shows the open request. Both deletions need--confirmwith the exact slug, checked before any request; in assisted mode the refusal gives the human’s command, and approving needs--confirmtoo. After the deletion,varlatch audit export --deleted-org <organization-id>exports the deleted Organization’s audit log for its readers.
- The dashboard’s
.envdownload no longer writes values that tools would change or run. It put values in double quotes without escaping$or backticks, and wrote a value containing&without quotes. So loading the file with a shell or a variable-expanding loader could expand$NAMEor run part of a value. No single.envform is read the same way by every common loader: dotenv-expand and python-dotenv expand$even inside single quotes, dotenv-expand 1000 also runs$(…), and dotenv before 15 reads a line break inside a value as the start of a new variable. Values are now unquoted or single-quoted. In that form, shells (sh, bash, zsh), dotenv, dotenv-expand, python-dotenv and Docker Compose read every written value exactly. A value containing$, a single quote, a line break, a backslash pair, a trailing backslash or a control character is left out of the file, with a comment in its place. The dialog names these values: plain values before the download, and secrets after it, since their values are only known once disclosed. Pasting a downloaded file into the dashboard, orvarlatch import, reads the same values back. - Reading values no longer re-runs an environment’s integrations. Viewing values in the dashboard, a strict retrieval, a validation, and every other audited read or denied action naming an environment made each integration of the environment decrypt its values and record an audit event on the next pass, with nothing to push. Only configuration changes now do (#59).
- Replacing a rejected credential resumes the integrations it stopped.
An integration stopped after its failure budget, because the platform
refused the old credential, stayed stopped after the credential was
replaced, and still asked for a new one: each had to be resumed by hand.
Now the replacement resumes those and starts every integration the old
credential failed afresh, recording
sync.target_resumedwith the replacement as its cause. Integrations stopped for another reason stay stopped (#57).
Copies in env files
Section titled “Copies in env files”- Every
.envcopy is called a copy. The dashboard’s.envdownload always says that the file is a copy outside Varlatch: rotating or deleting a value in Varlatch does not change it, and anyone with the file can use a credential in it until it is rotated at its provider. The paste import says the pasted text still exists where it came from, and once the pasted drafts are saved a short checklist follows.varlatch importsays, after it stored values, that the file still holds copies (or, after--delete-source, that other copies may exist), with a checklist when Secrets were stored. - Reads for an export are recorded as such. The download now reads
plain values and discloses Secrets with the purpose
export, so the audit log shows “read for export” instead of “revealed”. It records a read, not a saved file. Older servers do not accept the purpose, and the dashboard then reads without one.
-
Sync Targets have
lastDrift(at,count, the first 50names,eventId), andsync.push_resultgainsdriftRepairedanddriftFailedwhen a run put drift back. -
The disclosure purpose
export(capabilitydisclosure.purpose.export), accepted byPOST …/disclosuresand, as?purpose=export, by the effective configuration’s values read, which records it invalue.disclosed. -
Sync Targets have
drained(no run can start another request) andrunStartedAt.POST …/sync-targets/{target}/pause?wait=trueanswers once the Target is drained, or after 30 seconds withdrained: false; the pause takes effect either way. Capabilitysync.pause-wait. -
DELETE /v1/organizations/{org}/members/{member}removes a member, or leaves withme;PATCHwith{ "role": "admin" | "member" }changes a role;GET …/members/{member}/reviewlists what to review before a removal. Capabilitymembers.lifecycle. The audit events aremembership.removedandmembership.role_changed. When concurrent changes keep getting in its way, a removal or role change gives up withSTATE_CHANGEDanddetails.retryable: true, having changed nothing. -
GET,POST /v1/organizations/{org}/deletion(withafterDays), andPOST …/deletion/{deletion}/approveand…/cancelmanage an Organization’s deletion (capabilityorganizations.delete); a write to a frozen Organization is refused withORGANIZATION_DELETING, and a deleted Organization’s key withORGANIZATION_DELETED. Readers export a deleted Organization’s audit log fromGET /v1/deleted-organizations/{organization}/audit-events, or as NDJSON, oldest first, from…/audit-events/export; Installation Admins list deleted Organizations and remove readers under/v1/installation/deleted-organizations. -
DELETE /v1/organizations/{org}/projects/{project}deletes a Project and returns the counts (capabilityprojects.delete); the audit event isproject.deleted. -
GET /v1/organizations/{org}/members/{member}/exposurereturns the exposure report, andPOST …/exposure/confirmationswith aversionIdrecords a revocation at the provider (exposure.revocation_confirmed): it needsconfig.value.writewhere the value lives, or the built-in admin role once the value or its environment is deleted. -
Identities in an Organization’s listing have
removedAt, andorgRoleis null for a former member. -
A Grant for a person who is not a live member of the Organization, or for a machine of another Organization, is refused with
RESOURCE_NOT_FOUND.
Documentation
Section titled “Documentation”- The host requirements state how long deleting an organization takes,
measured at 100,000 and 1,000,000 value versions on a 2-CPU database, and
what it does to other organizations meanwhile: no write failed and no
lock wait was seen in sampling; some writes waited on the disk.
scripts/measure-org-deletion.mjsmeasures your own hardware.
Upgrading
Section titled “Upgrading”Database schema: migration 32 adds sync_runs, one row per push in
progress, and the repair-cycle columns sync_targets.repair_cycle_started_at
and sync_ledger.repaired_at; migration 33 adds sync_targets.redeploy_owed
and redeploy_paid; migration 34 adds the last_drift_* columns; migration
35 adds org_memberships.removed_at and version,
credentials.issued_by (filled in for existing credentials from their
issuance audit events), the removal snapshot tables and
member_integrations; migration 36 adds the exposure index
(member_exposure), its backfill state and exposure_confirmations. After
the upgrade, varlatchd indexes the audit events recorded before it in the
background, in small batches that resume after a restart. Migration 37
adds projects.deleted_at (a Project’s slug is now unique among live
Projects only) and value_versions.erased_at, with a version’s payload and
key nullable once erased. Migration 38 adds organization_deletions, the
suspension markers on identities and webhooks,
organizations.org_kek_destroyed_at (the wrapped key becomes nullable) and
audit_tombstone_readers. Every varlatchd process now runs a loop that
completes due deletions.
An item with a rotation hint needs this release on both sides: older
servers refuse a Contract that uses rotationHint, and older CLIs refuse it
in varlatch types. Upgrade the server and every CLI that generates types
before adding a hint.
Backups: archives of 0.19.1 and earlier still restore into this release. From 0.18.1 or 0.19.0, read the 0.19.0 and 0.19.1 notes below as well.
0.19.1 (2026-10-29)
Section titled “0.19.1 (2026-10-29)”Documentation
Section titled “Documentation”- Set up with a coding agent
(docs/set-up-with-an-agent.md): a page
to hand to your coding agent. It installs the CLI of the release your
installation runs, after checking its signature and checksum, starts the
sign-in for you to approve with your passkey, connects the repository,
moves
.envfiles in after showing you their names, and checks each environment. Setting up the server stays with you.
- Dependency update for a published advisory:
@hono/node-server2.1.5 (GHSA-rmxm-3fg6-px4f:serveStaticdecoded the request path twice, so middleware on a static prefix could be skipped). Varlatch never usesserveStatic, so no installation was exposed; varlatchd and the MCP server’s dependencies move to the fixed release. The Node.js adapter’s 2.x API is unchanged, and it needs Node.js 20 or later, which Varlatch already requires.
Upgrading
Section titled “Upgrading”Database schema: unchanged, migration 31. From 0.18.1, read the 0.19.0 notes below as well.
0.19.0 (2026-10-28)
Section titled “0.19.0 (2026-10-28)”Self-hosting
Section titled “Self-hosting”-
Update the pinned Convex backend to the official October 28 build (
cccbade43d6ca1e6023d085011d4b88887fc3224), which includes Node.js 24.22.1 and the CVE-2026-111497 Brotli decoder fix. The deployment job uses the same backend image for its key tool. -
The dashboard shows when a newer release exists. Installation settings has a Release tab, for Installation Admins: the version this installation runs, the latest release the host last found and when, its release notes, and the command to upgrade on the host (with
--repowhen the check used another repository). A newer release puts a dot on Installation settings in the organization menu and on Version in Settings. The dashboard does not upgrade anything, and neither varlatchd nor the browser contacts GitHub for it:varlatch upgrade --check, run without a version or--release-dir, now records what it found through the running varlatchd, and says whether it did. The reference backup timer runs the check after each successful backup, as a step that never fails the run (RELEASE_CHECK=offskips it). The tab says “No newer release as of” a date, never “up to date”, and stops vouching for that after 14 days; a newer release stays shown until the installation runs it. See Newer releases in the dashboard. -
varlatch doctorreportsrelease.latest, advisory and outside the upgrade gate, from the same record. -
A release lookup gives up after 30 seconds.
varlatch upgradeandvarlatch self-updatewaited on GitHub without a limit. They now name the cause when GitHub is unreachable or its rate limit is reached, and exit 69 when GitHub cannot be reached or answers 502, 503 or another server error, as for the Varlatch server; a server error exited 1 before.
- The CLI warns when its Node.js lacks security fixes. On a Node.js
release older than 22.23.4, 24.22.1 or 26.12.1 in its line (the security
releases of 21 October 2026, which fixed CVE-2026-111497 in Brotli
decompression, among others), on a line that no longer receives security
fixes, such as 20, 23 or 25, or on a prerelease, whose fixes its version
cannot show, every command first prints one line on stderr. Its output
and exit status do not change, and
varlatch agents hookstays quiet. Update Node.js, or setVARLATCH_NODE_CHECK=offwhen your Node.js carries the fixes under an older version number. See Node.js version.
GET /v1/installation/release(Installation Admins, capabilityinstallation.release): the running version, the host’s last successful check and last attempt, andnewer, compared with the running version when asked.
-
Dependency security updates: Better Auth and its passkey plugin 1.7.7, Zod 4.6.7, MCP SDK 1.32.2, and patched transitive body-parser, fast-uri, and qs versions. The documentation build also uses patched Zod and Undici versions. Both dependency audits now run in local
pnpm verify, with the same moderate-severity threshold as CI. -
Dependency update for a published advisory:
ip-address10.7.4 (GHSA-rxp7-6hxx-x8q5, quadratic-time matching of an IPv6 zone identifier), through the MCP server’s dependencies and bundled in the CLI.
Upgrading
Section titled “Upgrading”Database schema: unchanged, migration 31. To see the Release tab filled in
before the next nightly backup, run varlatch upgrade --check --dir /YOUR/COMPOSE/DIRECTORY once on the host after upgrading.
Before installing the new CLI, compare node --version on each machine
that runs it, the host included, with
Node.js version.
0.18.1 (2026-10-22)
Section titled “0.18.1 (2026-10-22)”Integrations
Section titled “Integrations”- Values Coolify resolves. An application integration can list names
whose values Coolify is to interpret, such as
{{team.GITHUB_TOKEN}}(a team shared variable) or$SOURCE_COMMIT: Names Coolify resolves on the Destination step, Let Coolify resolve it on a held name,platformExpandsin the API’s destination, or--platform-expands NAMEforvarlatch sync push. Varlatch stores those as Coolify stores its own variables (neither literal nor multiline); every other value is still stored as text, and Varlatch never infers the choice from$or{{. In 0.18.0, such a value was refused, or (for$) rewritten as literal text at the next repair. See Integrations. - A value that is not pushed no longer stops an integration. In
0.18.0 a value Coolify cannot carry counted as a failed push, and after
ten the integration stopped, with every value. Now the push succeeds for
the others, the card shows Values held with each name, its reason
and its fix, and nothing retries until the value changes or the next
periodic repair. A name Coolify sets itself on a service is held the
same way instead of failing a wildcard integration. The details are in
the integration’s
lastNotes(held, withfix), for applications too.
Upgrading
Section titled “Upgrading”Database schema: unchanged, migration 31. Check SHA256SUMS and its
signature as Verifying a release
describes.
- From 0.18.0: download
varlatch-cli-0.18.1.cjsfrom thev0.18.1release, check it againstSHA256SUMS, and runnode varlatch-cli-0.18.1.cjs upgrade 0.18.1 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.18.1.cjs, or runvarlatch self-update. An integration that 0.18.0 stopped because of a value it would not push: Resume it, and for a Coolify reference, list the name for Coolify to resolve first. - From 0.17.4 or older: follow the 0.18.0 notes below with the 0.18.1 CLI
and version. The upgrade applies migration 31, and there is no way back
to 0.17 but restoring an archive taken before it. Before the first
periodic repair writes your Coolify application variables again (within
six hours), list the names whose values are meant for Coolify to resolve
(
{{team.…}},$SOURCE_COMMIT): otherwise a{{…}}value is held and a$value is stored as literal text.
0.18.0 (2026-10-21)
Section titled “0.18.0 (2026-10-21)”Integrations
Section titled “Integrations”- Coolify services. An integration can now push to a Coolify service
(a one-click service or a Docker Compose stack), not only to an
application: choose Service on the Destination step, or pass
--service <uuid>tovarlatch sync pushandvarlatch sync check. A service’s variables reach every container of the service, its databases included, and the review says so. Names Coolify sets itself (SERVICE_FQDN_*,SERVICE_URL_*,SERVICE_NAME_*,COOLIFY_*) cannot be pushed to one, and its redeploy is a restart of the whole service, off by default. With Coolify’s Read sensitive permission, Varlatch reads the service’s compose file on every push and notes on the integration card where it overrides a pushed value; such notes never fail a push. Without it, a service value containing$is not pushed, since Varlatch cannot rule out that the compose file copies it where Docker Compose expands$. See Integrations. - A removed name Coolify re-creates is deleted once. When a service’s compose file references a name, Coolify creates it again at its next start; Varlatch no longer deletes it again on every repair (nor restarts the service each time), and notes it instead.
Changes for Coolify applications
Section titled “Changes for Coolify applications”- Values are written literal and multiline, so Coolify quotes them
instead of expanding
$or cutting them at#. Existing variables are written again once, on the next periodic repair, to get these settings. A value Coolify cannot carry or would change (surrounding whitespace, a single quote, a trailing backslash,{{environment.…}}, unquoted JSON with$) is no longer pushed changed, nor accepted when Coolify already holds it: the integration reports it and keeps what Coolify has, and the other values still go. See Values Coolify cannot carry. - A value Coolify withholds is unreadable, not empty. Without Read
sensitive, Coolify lists variables without their values. Varlatch read
those as empty strings; repair now rewrites them without redeploying,
and
varlatch sync checkreports them as not compared (platform-withheld) without disclosing the Secret to compare.
- Every Sync Target has
lastNotes: the notes of its last run that inspected the destination (shadowed,not-delivered,unknown-form,generated-once,kept-by-platform,usage-unchecked),nullwhere nothing was inspected. A Coolify service destination takesserviceUuidinstead ofapplicationUuid(application destinations are unchanged), and destination listings include services.
Upgrading
Section titled “Upgrading”Database schema: migration 31 (a Sync Target’s notes). It only adds a
column, empty for every existing Target. Check SHA256SUMS and its
signature as Verifying a release
describes.
- From 0.17.0 to 0.17.4: download
varlatch-cli-0.18.0.cjsfrom thev0.18.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.18.0.cjs upgrade 0.18.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive while the installation keeps serving, applies migration 31, and completes only once the new release passes its health gate. Then replace the host CLI withvarlatch-cli-0.18.0.cjs, or runvarlatch self-update. - From 0.16.0 or older: follow the 0.17.0 notes below with the 0.18.0 CLI and version. The upgrade also applies migration 31.
- Back up first: there is no way back to 0.17 but restoring an archive taken before the upgrade. 0.17 refuses a database with migration 31, and everything written after that archive is lost.
- Coolify application integrations: for each active integration, the first
periodic repair after the upgrade (within six hours) writes every managed
variable again, literal and multiline. The next deployment of each
application then receives its values exactly as stored: a value that
relied on Coolify expanding
$($OTHER,${OTHER}) arrives as written. Afterwards, check the integration cards: a value the new rules refuse is held, Coolify keeps what it had, and the card names it. Each attempt with a held value counts as a failed one, so after ten the integration stops: correct the value, then Resume the integration (correcting the value alone does not restart a stopped one). - 0.18.0 restores everything 0.17.4 restores, and archives from 0.17.4.
0.17.4 (2026-10-21)
Section titled “0.17.4 (2026-10-21)”Self-hosting
Section titled “Self-hosting”- The Convex backend is pulled from its new registry path. Convex
moved its images from
ghcr.io/get-convex/toghcr.io/convex/, and the old path now refuses downloads, so a fresh 0.17.3 installation could not start its Convex backend. The compose files now nameghcr.io/convex/convex-backendwith the same digest: the same image, byte for byte. An existing installation keeps running on its local copy; upgrading to this release lets it download the image again when it needs to.
Dependencies
Section titled “Dependencies”@modelcontextprotocol/sdk1.32.1,hono4.13.13, andvite7.3.7 (8.3.4 for the documentation site), for newly published advisories. None reaches Varlatch as it uses these libraries: its MCP server speaks only over stdio,varlatchddoes not serve static files throughhono, and theviteissues are in a Windows-only development path.
Documentation
Section titled “Documentation”- The
.env.schemacontract file says where its format comes from: @env-spec, created by DMNO for Varlock. Varlatch reads the subset the page lists with its own parser and does not follow later changes to @env-spec.
0.17.3 (2026-10-13)
Section titled “0.17.3 (2026-10-13)”Dashboard
Section titled “Dashboard”-
The Tailnet tab names the node. Installation settings, Tailnet shows the node under Observed, dated by the report’s one “Last checked”: its machine name, tailnet, tags, Tailscale addresses and StableID. With them, the access rule for the browser endpoint is filled in the way
varlatch setupfills it: the node’s tags, or its IPv4 address when it has none, on the configured endpoint’s port. The tab says that a tag as the destination covers every node carrying that tag. The Coolify fixes for a renamed node or a node on another tailnet name the node’s actual name and tailnet, and the endpoint lines name the node when it is on the configured tailnet. Without usable node details (an older server, or a failed read), the tab says so and keeps the placeholders. -
Every network requirement can be edited in the dashboard. Access, Advanced edits the whole requirement: what it applies to (a tier, or environments, where a root environment also covers those derived from it), the tailnet, and the devices (node IDs), tags and users a device may match. Requirements scoped to environments or naming devices or users, which the form could not edit before, are editable now, and every field the edit does not change is saved as it was. An edit is saved only after a review that shows the requirement before and after, names the tier or environments it applies to, and lists what may loosen and what tightens. Anything that may loosen (an added device, tag or user, a different tailnet, values no longer covered, a tier no longer covered as a whole, or environments you cannot see) needs the explicit “Loosen and save”. When the requirement changes while you edit it, seen live or refused by the server, the review and its confirmation are discarded, nothing is saved, and you start over from the current version. Tags follow Tailscale’s rule.
-
“Add this device” in the requirement editor. With the tailnet browser endpoint, the editor can add the device this browser is on: it runs Connect and uses only an answer newer than the click, then adds the node ID Varlatch verified for it, and nothing else (no tag, no user). An answer that is older, failed, unrecognized or late adds nothing and says why. A device on another tailnet than the requirement’s is not added: the editor offers to change the requirement’s tailnet as a change of its own, which the review shows as one that may loosen. Adding a device is itself reviewed as loosening.
-
A suggested tailnet for a new requirement. The editor fills in the tailnet of the device this browser is verified as, or else the one in the browser endpoint’s address, and says which. It stays editable and stops following suggestions once you type in the field. Existing requirements are never changed by it.
GET /v1/installation/listeners(Installation Admins) reportstailnet.observed.node: the node as varlatchd last read it from the LocalAPI status (name,dnsName,tailnet,tags,addresses,nodeId). Each field is validated and omitted when absent or malformed; a list with any bad entry is omitted whole. A failed read clears it, and an older check never replaces newer details. It says nothing about who can reach the node. Protocol:ObservedNode.
Upgrading
Section titled “Upgrading”No database migration. Check SHA256SUMS and its signature as
Verifying a release describes.
- From 0.17.2: download
varlatch-cli-0.17.3.cjsfrom thev0.17.3release, check it againstSHA256SUMS, and runnode varlatch-cli-0.17.3.cjs upgrade 0.17.3 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.17.3.cjs, or runvarlatch self-update. Going back to 0.17.2 needs no restore: the database is the same. - From 0.17.0 or 0.17.1: the same command; neither added a migration.
- From 0.16.0 or older: follow the 0.17.0 notes below with the 0.17.3 CLI and version. The upgrade applies migrations 28 to 30.
- No requirement changes on upgrade. The editor only changes one when someone saves an edit after its review.
- 0.17.3 restores everything 0.17.2 restores, and archives from 0.17.2.
0.17.2 (2026-10-13)
Section titled “0.17.2 (2026-10-13)”Dashboard
Section titled “Dashboard”- Installation settings show the tailnet listeners. A new Tailnet tab
(
/installation/settings?tab=tailnet, for Installation Admins) shows whatGET /v1/installation/listenersreports: the configured tailnet, listener port and browser endpoint, then varlatchd’s own checks (listeners, LocalAPI, node, endpoint certificate) as pass, fail or unknown with the reason, the certificate’s expiry, and one “last checked” age for the whole report. A report older than 10 minutes is flagged, as the server’s doctor flags it. The tab says these are server-side checks: whether a device reaches a listener depends on the tailnet policy, and only that device can tell, with Connect to tailnet. Without a tailnet listener it says so, and claims nothing about Tailscale elsewhere on the host. Forvarlatch setupor Coolify, chosen on the tab, it shows the commands or variables that add the Tailscale sidecar or turn on the browser endpoint, a fix for each failing check, and the access rulevarlatch setupprints, with a placeholder for the node. Read-only: the dashboard still changes neither the deployment nor the tailnet policy. Backups are the page’s other tab;/installation/settingsstill opens them. SDK:getInstallationListeners().
Upgrading
Section titled “Upgrading”No database migration. Check SHA256SUMS and its signature as
Verifying a release describes.
- From 0.17.1: download
varlatch-cli-0.17.2.cjsfrom thev0.17.2release, check it againstSHA256SUMS, and runnode varlatch-cli-0.17.2.cjs upgrade 0.17.2 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.17.2.cjs, or runvarlatch self-update. Going back to 0.17.1 needs no restore: the database is the same. - From 0.17.0: the same command. 0.17.1 added no migration either, and its Coolify fix comes with this release.
- From 0.16.0 or older: follow the 0.17.0 notes below with the 0.17.2 CLI and version. The upgrade applies migrations 28 to 30.
- 0.17.2 restores everything 0.17.1 restores, and archives from 0.17.1.
0.17.1 (2026-10-10)
Section titled “0.17.1 (2026-10-10)”- Migrate runs on Coolify with the tailnet browser endpoint turned on.
Coolify hands every environment variable of an app to every service, so
varlatch-migratereceivedVARLATCH_TAILNET_HTTPS_PORTwithout the tailnet listener settings only varlatchd gets, refused to run (“needs the tailnet listener”), and varlatchd, which waits for it, never started. Migrate no longer checks listener settings: it serves nothing. Plain Compose installations were not affected.
Upgrading
Section titled “Upgrading”No database migration. Check SHA256SUMS and its signature as
Verifying a release describes.
- From 0.17.0: download
varlatch-cli-0.17.1.cjsfrom thev0.17.1release, check it againstSHA256SUMS, and runnode varlatch-cli-0.17.1.cjs upgrade 0.17.1 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.17.1.cjs, or runvarlatch self-update. Going back to 0.17.0 needs no restore: the database is the same. - From 0.16.0 or older: follow the 0.17.0 notes below with the 0.17.1 CLI and version. The upgrade applies migrations 28 to 30.
- On Coolify, turn on the tailnet browser endpoint only on 0.17.1 or later.
- 0.17.1 restores everything 0.17.0 restores, and archives from 0.17.0.
0.17.0 (2026-10-10)
Section titled “0.17.0 (2026-10-10)”Machine identities
Section titled “Machine identities”- A credential can say whose it is.
GET /v1/me(capabilityidentity.whoami) tells any authenticated caller, a person or a machine, which identity its credential belongs to (id,name,kind, and for a person the profile’semail), that identity’s organization (null for a person, who joins organizations as a member), the credential the request presented (id,name,kind,expiresAt; never the token, never the identity’s other credentials), and the listener the request came in on. On the tailnet listeners it addstailnet: the device WhoIs resolved the connection to, with its machine name (nodeName), orrecognized: falseand why, exactly asGET /v1/tailnet/contextreports it. It needs no grant and describes the caller only. It is not a cross-origin route on the tailnet browser endpoint. Before, a machine could not find out:/v1/me/profileis for people,/v1/me/credentialslists credentials by name, and/v1/organizationslists memberships, which machines have none of. Now a program that shares its host’s identity with others can, for instance, find the host’s own environment from the identity’s name. A successful call records no audit event, like the other/v1/mereads; a refused credential (revoked, expired, spent, or of a retired identity) gets 401 and is recorded asauthentication.failed, as everywhere. SDK:whoami(). - CLI:
varlatch whoami [--server <url>] [--json]prints the identity, organization, credential, listener, and device for the credential the CLI would use:VARLATCH_TOKEN, else the stored one. The server is--server,VARLATCH_SERVER, the repository’s, or else the only one with a stored credential. Against an older server it says so and exits 1.varlatch status --probe --jsonadds each valid credential’sidentityandorganizationwhere the server can say; the human format ofstatusdoes not change. - MCP:
varlatch_whoamiaddscaller, the same answer, ornullwithcallerUnavailablesaying why (an older server, a refused credential). - For programs that rely on it: fall back to another way of finding
the identity only when the server lacks
identity.whoami. A refused credential, a server error, or an identity that is not the expected one (organization, naming convention) is an error to report: identities can be renamed. Each call is an authenticated request, so it spends a use of a use-limited credential; ask once per process and keep the answer.
Tailnet
Section titled “Tailnet”varlatch setup --ingress tailnetworks on a fresh installation again. Since 0.14.3, Compose requiresVARLATCH_PUBLIC_URL, which the tailnet ingress learns only once its node has joined; setup stopped before the node could join (“required variable VARLATCH_PUBLIC_URL is missing a value”). The join now runs with a placeholder in its own commands’ environment, never in.env. Existing installations were not affected.- Allowed reads say which network requirement let them through. Audit
events for value reads, disclosures, validation and strict retrievals
(and a capability exercise’s reference expansions) now carry
authorization: the Grants that applied and, for each network requirement that applies, its outcome and what satisfied it (for example"by": "node:nLAPTOP"), as denials already do. Where no requirement applies, the list is empty. - The tailnet listener refuses two more kinds of device. A device shared into the tailnet from elsewhere (WhoIs says who shared it) and the Varlatch node itself get no Tailnet Context any more, whatever their name, tags or user, so they cannot satisfy a network requirement. Before, a shared device was refused only when its name gave away another tailnet, and the node itself was treated like any other device. If one of your requirements names the Varlatch node, its tags, or (for an untagged node) the user who registered it, those matches stop working.
- The audit log says which listener a request came in on, and from which
device. Events a request records for its own identity now carry
listener(ordinaryortailnet) and, on the tailnet listener,tailnet: the device WhoIs resolved, or why there was none ({"refused": ...}:resolver-unavailable,unrecognized,other-tailnet,shared,self). Before, only strict retrievals and capability exercises recorded a listener, and nothing recorded the device. - varlatchd can serve a tailnet browser endpoint, off by default. With
VARLATCH_TAILNET_HTTPS_PORT(andVARLATCH_TAILNET_MACHINE, the node’s machine name), varlatchd serves the tailnet listener over HTTPS on that port too, with the node’s own Tailscale certificate, which it fetches from tailscaled and keeps in memory. A browser on an approved device can then read tailnet-protected values: the device is checked on each request’s own connection, as on the plain tailnet listener, and the bearer as everywhere. Only exact origins may read cross-origin: the dashboard’s HTTPS origin, plus any inVARLATCH_TAILNET_BROWSER_ORIGINS, and only for value reads, disclosures, validation andGET /v1/tailnet/context. Other origins and routes get 403 before authentication; requests without anOrigin(the CLI) are unaffected. The dashboard does not use it yet, andvarlatch setupdoes not configure it yet. Nothing changes until the port is set. - The dashboard reads tailnet-protected values from an approved device. Where the installation serves a tailnet browser endpoint, the notice in place of Reveal offers “Connect to tailnet”. Nothing goes to the endpoint before that; the first request checks this device, and the browser may ask to allow access to the local network. After that, in that tab only, protected environments’ values, reveals and exports go to the endpoint, which checks the device on every request; navigation and everything else stay where they were. Once an environment is known to need the tailnet, its values are never asked for on the dashboard’s own address. When the endpoint stops answering, a reveal is not sent again: the dashboard says so and checks the connection. The notice says what was configured, what Varlatch checked, and what this browser could reach.
- The dashboard is served with a strict Content-Security-Policy:
scripts and styles from its own address only, nothing inline, no
framing, and connections only to itself, the Convex address it uses
and, when set, the tailnet browser endpoint (
VARLATCH_TAILNET_ENDPOINTon thevarlatch-webcontainer). The container refuses to start whenCONVEX_URLorVARLATCH_TAILNET_ENDPOINTholds anything but a plain URL. varlatch setup --tailnet-endpointturns the tailnet browser endpoint on (--no-tailnet-endpointturns it off). Setup writes the settings varlatchd, the dashboard and the Tailscale sidecar need (the sidecar lets varlatchd’s user, uid 999, fetch the node’s certificate and nothing else), requires HTTPS certificates for the tailnet, and prints a tailnet access rule for port 8688 to start from; it never edits the tailnet policy. With the public or external ingress it adds the Tailscale sidecar without changing the public address.varlatch doctorreports the tailnet listener and the browser endpoint as varlatchd last observed them (listening, certificate and its expiry, LocalAPI, node running on the pinned tailnet under the endpoint’s name), whether the dashboard’s security policy allows the endpoint, and fails when Tailscale Serve claims the endpoint’s port. Whether a browser can reach the endpoint stays unknown: only a browser can tell.- API:
GET /v1/installation/listeners(Installation Admins): the tailnet listeners as configured and as varlatchd last observed them, with the time. - varlatchd’s image pins its user to uid 999 (it already was).
- On the tailnet listener, each recognized device now has its own request window. Before, every tailnet client shared one, since all of them arrive from the sidecar’s 127.0.0.1.
- API:
GET /v1/tailnet/endpoint(any authenticated caller: the browser endpoint’s URL, or null),GET /v1/tailnet/context(on the browser endpoint only: the caller’s verified device, or why there is none), and the capabilitytailnet.browser-reads, advertised when the endpoint is configured.
Integrations
Section titled “Integrations”- Editing an integration while its connection’s credential is replaced, or while the connection is revoked, no longer fails with an internal error. The two took their database locks in opposite orders and could deadlock; the edit now waits for the other change, and is refused as out of date when the connection was revoked meanwhile.
- Varlatch checks a platform credential before it saves anything: when a
connection is created, when its credential is replaced (against every
integration that uses it), and on the review step of a new integration,
against the chosen destination. A failed check names the problem and the
part to fix (a rejected or expired token, a missing permission, an
unknown owner, repository, environment, application, or deployment, or a
platform that cannot be reached), and the dialog can still save anyway.
A check only reads, stores nothing, and is audited as
Access checked. - API:
POST /v1/organizations/{org}/platform-connections/check, for a new credential or a connection’s stored one, with an optional destination. - New guide, Integrations: the credential each platform needs, with its permissions, and where to find every field. The GitHub hint now says that environment secrets need the Environments permission, not Secrets.
- The Destination step lists what the connection’s credential can see:
GitHub repositories in its owner, or the applications of a Coolify
token’s team, by name. Picking one fills the field; typing still works
for a destination the list leaves out or that does not exist yet, and
Review still checks it. A listing only reads, keeps each application’s
uuid, name, and address only, and is audited as
Destinations listedwith a count, never names. - API:
POST /v1/organizations/{org}/platform-connections/destinations, for a new credential or a connection’s stored one. - GitHub token expiry: GitHub reports when a personal access token expires, and Varlatch records it on the connection whenever it uses the stored token (a push, a check, a listing). From two weeks ahead, the connection and every integration that uses it warn, with a way to replace the credential; an expired token says so. Creating a connection or replacing its credential with a token that expires within two weeks stops first, with Save anyway or Replace anyway. GitHub does not repeat the date on every answer, so a connection’s known date stands in for its stored token. Replacing the credential clears the date until the new token is used. A token without an expiry, and other platforms, show no date.
- API: a connection carries
credentialExpiresAtandcredentialExpirySeenAt(null when unknown), and an access checkcredentialExpiresAtwhen GitHub reports it. - Database migration 28 adds the connections’
credential_expires_atandcredential_expiry_seen_atcolumns, nullable and unknown for every existing connection until its token is next used. Backup archives of 0.16.0 (migration 27) restore into this release. - Database migration 29 prepares GitHub App connections: a
github_appstable and a credential kind on connections (tokenorgithub-app). Every existing connection becomestokenand works as before. Backup archives of 0.16.0 restore into this release. - API: an Organization can register its GitHub App through GitHub’s
manifest flow (
POST /v1/organizations/{org}/github-app/registrations, then.../registrations/completewith GitHub’s code) and read it (GET /v1/organizations/{org}/github-app). Varlatch keeps the App only when GitHub created it on the intended account. When the person may not register Apps there, GitHub creates it on their own account instead; Varlatch then keeps nothing, and the answer says where to delete it on GitHub. The App’s private key rests under the Organization KEK and is never returned. Database migration 30 holds registrations in progress (only a hash of each, for an hour). - API: an Organization can instead import a GitHub App registered on
GitHub, by its App id and private key
(
POST /v1/organizations/{org}/github-app/import). Varlatch verifies the pair with GitHub first, takes the App’s details from GitHub, and needs the App to hold secrets and environments (write) and metadata (read). An App with more permissions is accepted, and the audit event names them; the stored key keeps all of them, even though the tokens Varlatch mints are narrowed to each use, so grant the App no more than it needs.GET /v1/organizations/{org}/github-app/installationslists where the App is installed. When GitHub refuses the App’s signed request, the answer says whether the key was refused or this server’s clock is too far off GitHub’s. - API: a GitHub Actions connection can now use the Organization’s GitHub
App instead of a personal access token:
POST /v1/organizations/{org}/platform-connectionswith{"credentialKind": "github-app", "installationId": ..., "name": ...}. Varlatch stores no token for it. Each push, access check, and listing gets a one-hour token from the App, limited to what it needs: one repository with write access to its secrets (or environment secrets) for a push, read access for a destination check, and only repository metadata for checking the connection alone or listing its repositories, which are exactly the installation’s. Such a connection never shows an expiry and has no credential to replace. Connections carrycredentialKind,githubAppId, andinstallationId. - API: the GitHub App’s key can be rotated
(
POST /v1/organizations/{org}/github-app/key, with the new private key and the App’s version). Varlatch checks the new key with GitHub first. The rotation re-authorizes every integration on the App’s connections at once, including paused and disabled ones, so you need permission to send each one’s values; otherwise nothing changes. Every integration then re-syncs with the new key, and you delete the old one on GitHub.DELETE /v1/organizations/{org}/github-appremoves the App from Varlatch: its connections are revoked, their integrations are disabled and keep their destinations, and the stored key is deleted. The App itself stays on GitHub until you delete it there. - The dashboard sets up GitHub through your own GitHub App. On Connections, register one (GitHub shows its form, then sends you back) or import one an owner registered, connect its installations, rotate its key, and remove it. If GitHub creates the App on your own account because you may not register Apps on the organization, Varlatch keeps nothing, says so, and links to where you delete it. A connection through the App shows no expiry, and fixing one rotates the App’s key. The dashboard’s Content-Security-Policy now lets forms post to github.com, for GitHub’s App form.
- SDK:
getGitHubApp,startGitHubAppRegistration,completeGitHubAppRegistration,importGitHubApp,listGitHubAppInstallations,createAppConnection,rotateGitHubAppKey, andremoveGitHubApp.
Dashboard
Section titled “Dashboard”- Network requirements show everything they enforce. Access, Advanced names each requirement’s environments by project and name (the ID when an environment is not visible to you), notes that a requirement on a root environment also covers the environments derived from it, and lists every device (Tailscale node ID), tag, and user it accepts. It also says how they combine: a device passes a requirement by matching any one of them, and every requirement that covers a value must pass. Before, a requirement for specific environments read “1 environments”, and its devices and users were not shown.
- Edit is off for requirements the form cannot represent. The form edits a tier and its tags. Saved over a requirement for specific environments, or one that names devices or users, it replaced them with a tier and tags, which could let more devices read the values. Those requirements now say why Edit is unavailable; change them through the API.
- Tailnet-only values are marked where they are used. A network
requirement keeps every value of the environments it covers, non-secret
ones included, to verified devices on the tailnet, and the dashboard
never connects through the tailnet. Those environments now carry a
“Values: tailnet only” badge in their header and their values grid
column. Instead of Reveal (one item, a column, Reveal all, the palette’s
?reveal=1, and export) they explain where the values can be read. Viewers who may see access policy get the requirements and a link to them; others get the general guidance. Before, the environment page showed “Values unavailable” with the server’s error and recorded a denial in the audit log on every visit; now it shows the items and their states and asks for no values. Secrets revealed on a page before a requirement covers its environment stop showing, and can no longer be copied, as soon as the page learns of the requirement. A reveal still in flight then is discarded, and stays discarded if the requirement is removed before it lands. An export dialog already open drops the values it loaded and stops offering Download, and an export in flight writes no file. This tidies the screen only: plaintext the browser already received is not revoked.
- Allowed actions say when the Organization role let them through. An
allowed event’s
authorizationnow carriesrole(adminormember) when the actor’s built-in Organization role allowed the action, next to the Grants that applied. Before, an admin’s action recorded only an empty Grant list, as if nothing had allowed it; the dashboard’s audit log now says “Allowed as organization admin.” for it. Integrations record it too: creating or re-pointing an integration, replacing a connection’s credential, and rotating the GitHub App’s key record, for each Target, the role or Grants that passed its disclosure gate. Requirement outcomes are recorded as before.
- Environments carry
tailnetRequired(capabilityenvironments.tailnet-required): true when a network requirement covers the environment, so that reading its values needs the tailnet listener. It is worked out on each response with the same targeting authorization uses: a tier, the environment, or the root it derives from.tailnetRequirementIdslists the covering requirements, for callers withpolicy.read.
Upgrading
Section titled “Upgrading”Database schema: migrations 28 (the connections’ credential expiry), 29
(GitHub Apps, and a credential kind on connections) and 30 (GitHub App
registrations in progress). All three only add: every existing connection
becomes a token connection and works as before. Check SHA256SUMS and
its signature as Verifying a release
describes.
- From 0.16.0: download
varlatch-cli-0.17.0.cjsfrom thev0.17.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.17.0.cjs upgrade 0.17.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive while the installation keeps serving, applies migrations 28 to 30, and completes only once the new release passes its health gate. Then replace the host CLI withvarlatch-cli-0.17.0.cjs, or runvarlatch self-update. - From 0.15.2 or older: follow the 0.16.0 notes below with the 0.17.0 CLI and version. The upgrade applies migrations 27 to 30.
- Back up first: there is no way back to 0.16.0 but restoring an archive taken before the upgrade. 0.16.0 refuses a database with migrations 28 to 30, and everything written after that archive is lost.
- Before upgrading, check that no network requirement names the Varlatch node, its tags, or (for an untagged node) the user who registered it: the node itself no longer satisfies a requirement, and neither does a device shared into the tailnet from elsewhere (see Tailnet).
- The dashboard is served with a strict Content-Security-Policy. Its
container refuses to start when
CONVEX_URL(fromCONVEX_CLOUD_ORIGIN) is not a plain http(s) URL, and anything you added to the dashboard that loads from another origin is blocked. - Registering a GitHub App needs the installation’s public URL
(
VARLATCH_PUBLIC_URL) over HTTPS. - Programs that ask
GET /v1/mewho they are should follow the rules under Machine identities: fall back only when the server lacksidentity.whoami. - 0.17.0 restores everything 0.16.0 restores, and archives from 0.16.0.
0.16.0 (2026-10-08)
Section titled “0.16.0 (2026-10-08)”Machine identities
Section titled “Machine identities”- Issue another credential for an existing service, workload, or broker
identity. Several programs on one host can share the identity and its
grants, each with its own credential, listed under its name and revoked
on its own. It is also how a reactivated identity gets a working
credential again, and the first step of replacing one: issue the new
credential, move the program to it, then revoke the old one.
ciandagentidentities, which never hold a stored credential, and retired identities get none. - CLI:
varlatch credential issue <identity-id> --name <name> [--ttl <s>] [--max-uses <n>] --out <file> [--json]. The token is written only to--out, a new file created with mode 0600 in a directory that exists; an existing file is refused. The token never appears in the output or in an error, so the command works in assisted mode too. Against an older server it says so and issues nothing. - Dashboard: a machine’s Credentials panel, on the Machines tab, has an Issue credential action with a name and an optional lifetime. The token is shown once, with a copy button.
- The audit log says which credential and client acted. Every event an
authenticated request records for its own identity now names the
credential that made the request (
credentialId, before only on denials) and the request’s client (client, new), a short summary of its User-Agent:Firefox on Linuxfor the dashboard,varlatch CLI 0.16.0 on Linuxfor the CLI,varlatch CLI 0.16.0 on Linux, assistedwhen a coding agent drives the CLI in assisted mode, andvarlatch MCP 0.16.0 on Linuxforvarlatch mcp. So the log tells a person’s dashboard from their CLI, a coding agent from the human, and one machine from another sharing an identity. The client is what the caller says, never verified, and no authorization depends on it. A label holds only fixed words and a release version, never free text from the header. Events with no authenticated request (failed sign-ins, sync delivery, webhooks, operator commands) and events from before the upgrade have neither. Oncredential.issued,credential.revoked, andauthentication.device_collected,credentialIdkeeps naming the credential the event is about. - The dashboard’s audit log shows how the actor connected next to their name (the client, else the credential’s name), and an event’s details show its credential (name, kind, and ID) and client. The actor filter groups people, agents, CI, and machines, marks retired identities, and offers Varlatch for Varlatch’s own events.
- The
credential.issuedevents of a machine identity’s credentials now appear in the organization’s audit log: the credential issued when the identity is created, the agent-run credentials a Broker mints, and the credentials of an OIDC exchange. Before, they were recorded without an organization, and no audit listing showed them.
- Every request the CLI makes now sends
varlatch-cli/<version> (<platform>; <arch>)as its User-Agent, with a third token,assisted, in assisted mode; before, onlyvarlatch loginsent one.varlatch mcpsendsvarlatch-mcp/<version> (<platform>; <arch>).
- Audit events carry
client, andcredentialIdon every event its request’s identity records (capabilityaudit.attribution). Both are additive:schemaVersionstays 1, and the listing, the NDJSON export, and webhook deliveries carry them. - The audit listing returns
credentials, the credentials its page’s events name, by ID:name,kind, andclientfor each, never token material. It belongs to the listing only, not to the event: the export and webhooks do not carry it. Merge it across pages. SDK:listAuditEvents()returns it (AuditEventPage). - The audit listing and export take the filter
actor=varlatch: only Varlatch’s own events, those without an actor identity, exceptauthentication.*events, whose missing actor is an unknown caller. WithactorIdentityIdit is refused withVALIDATION_FAILED. SDK: theactorfilter. - Database migration 27 adds the audit events’
clientcolumn, nullable. The runtime role keeps INSERT and SELECT only on the audit table. POST /v1/organizations/{org}/identities/{identity}/credentialshas a second path (capabilityidentity.credentials.issue). A caller withidentity.managesends{"name": ..., "ttlSeconds": ..., "maxUses": ...}, the last two optional with the limits identity creation takes, and gets{id, kind: "service", name, token, expiresAt, maxUses}once, withCache-Control: no-store. WithoutttlSecondsthe credential does not expire, as at creation. Human,ci,agent, retired, and other organizations’ identities are not found. A Broker’s request takes the agent-run path exactly as before; a caller withoutidentity.managenow getsPERMISSION_DENIEDthere instead ofRESOURCE_NOT_FOUND, as on the revocation route. The audit eventcredential.issuedrecords the actor, the identity, and the credential’s id, kind, name, expiry, and use budget, in the organization’s audit log. SDK:issueMachineCredential(). In the OpenAPI document the operation is nowissueIdentityCredential.
Upgrading
Section titled “Upgrading”Database schema: migration 27 (the audit events’ client). Earlier events
keep no client, and credentialId only where they had one. Check
SHA256SUMS and its signature as
Verifying a release describes.
- From 0.15.2, 0.15.1, or 0.15.0: download
varlatch-cli-0.16.0.cjsfrom thev0.16.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.16.0.cjs upgrade 0.16.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive while the installation keeps serving, applies migration 27, and completes only once the new release passes its health gate. Then replace the host CLI withvarlatch-cli-0.16.0.cjs, or runvarlatch self-update. - From 0.14.3 or older: follow the 0.15.0 notes below with the 0.16.0 CLI and version. The upgrade applies migrations 26 and 27.
- Update the CLI with
varlatch self-updatewherever coding agents run it: older CLIs send no User-Agent outsidevarlatch login, so their events carry the credential but no client.varlatch credential issueneeds the 0.16.0 CLI and server. - 0.16.0 restores everything 0.15.2 restores, and archives from 0.15.2.
0.15.2 (2026-10-08)
Section titled “0.15.2 (2026-10-08)”Sign-in
Section titled “Sign-in”- On Windows,
varlatch loginopens the sign-in page in the default browser; before, it only printed the address. It starts Windows’ URL handler directly, never through a shell. - With
VARLATCH_NO_BROWSERset to anything but an empty value or0,varlatch loginopens no browser and only prints the address, for an app that drives the sign-in and opens the address itself. The two lines it prints the address in do not change: Browser sign-in from an app. An older CLI ignores the variable.
Documentation
Section titled “Documentation”- Getting started shows how to
install the CLI on Windows: check it against
SHA256SUMSwithGet-FileHash, install it as%LOCALAPPDATA%\Programs\Varlatch\varlatch.cjswith avarlatch.cmdnext to it, and add that directory to yourPATH.varlatch self-updatethen replaces it without administrator rights.
- An agent-safe run removes an inherited
NO_PROXYorno_proxyentry that exempts every host (*,*with a port, or a/0address range such as0.0.0.0/0) and names it. Node 26.11 honours*anywhere in the list, so the Broker’s address the run appends no longer disabled an inherited*for it: a Node Agent’s requests went around the Broker, unblocked even with--agent-network strict. Other inherited entries still bypass the Broker, as Agent-safe runs states. - On Windows,
varlatch self-updateno longer suggestssudowhen it cannot replace the CLI. It suggests rerunning the command it shows from a terminal run as administrator, or installing the CLI in a directory you own, and closing any program that has the file open.
Releases
Section titled “Releases”- Image signatures are published in their own package,
ghcr.io/varlatch/varlatch-signatures, instead of beside each image. GitHub showed the newest signature as an image’s latest version, with a pull command for the signature instead of the image. To verify images, setCOSIGN_REPOSITORYas Verifying a release shows. 0.15.1’s signatures are in both places.
Upgrading
Section titled “Upgrading”No database migration: 0.15.2 runs on 0.15.1’s schema (migration 26). Check
SHA256SUMS and its signature as
Verifying a release describes.
- From 0.15.1 or 0.15.0: download
varlatch-cli-0.15.2.cjsfrom thev0.15.2release, check it againstSHA256SUMS, and runnode varlatch-cli-0.15.2.cjs upgrade 0.15.2 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.15.2.cjs, or runvarlatch self-update. - From 0.14.3 or older: follow the 0.15.0 notes below with the 0.15.2 CLI and version. The upgrade applies migration 26.
- Update the CLI wherever agent-safe runs start, with
varlatch self-update: the fix for an inherited*inNO_PROXY(see Fixes) is in the CLI, not the server. - 0.15.2 restores everything 0.15.1 restores, and archives from 0.15.1.
0.15.1 (2026-10-07)
Section titled “0.15.1 (2026-10-07)”Documentation
Section titled “Documentation”- The README states that Varlatch comes without warranty and that the licenses’ warranty and liability sections are the binding terms, and shows the dashboard, an agent-safe run, and the architecture.
- Verifying a release says which releases are signed by version instead of by their notes: 0.15.0 and earlier were published while the repository was private and are not signed, and every later release is. The install steps no longer describe downloading from a private repository, and the documentation site links to varlatch.com.
varlatch moveno longer stops when only recording its archive’s verification in the backup status fails. It verifies the archive again without recording, goes on when that passes, and says so (#1). A damaged archive still stops the move before anything changes.varlatch moveandvarlatch setupstop with a clear error whenvarlatch-move.jsonis damaged, instead of a raw JSON error. The move guide says how to find where the move stopped.
Upgrading
Section titled “Upgrading”No database migration: 0.15.1 runs on 0.15.0’s schema (migration 26).
0.15.1 is the first signed release: check SHA256SUMS and its signature as
Verifying a release describes.
- From 0.15.0: download
varlatch-cli-0.15.1.cjsfrom thev0.15.1release, check it againstSHA256SUMS, and runnode varlatch-cli-0.15.1.cjs upgrade 0.15.1 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.15.1.cjs, or runvarlatch self-update. - From 0.14.3 or older: follow the 0.15.0 notes below with the 0.15.1 CLI and version. The upgrade applies migration 26.
- 0.15.1 restores everything 0.15.0 restores, and archives from 0.15.0.
0.15.0 (2026-10-07)
Section titled “0.15.0 (2026-10-07)”Documentation
Section titled “Documentation”- Evaluated coding agents
now describes the evaluation of 0.14.3: Claude Code 2.1.278 with
claude-sonnet-5-5and Codex CLI 0.160.1 passed all 40 cases of ten tasks, including the short Secret a command does not need.
- A
varlatch runinside an agent-safe run reads at most 64 KiB when it asks the Broker how many tunnels it refused. It takes the Broker’s address from the Agent’s environment, so whatever answered there could otherwise stream an endless body into its memory. Past the limit, it prints no note, as for any other unusable answer.
Self-hosting
Section titled “Self-hosting”varlatch movegives an installation another public URL (#103), for a new domain or to go from a tailnet address to a public one. Passkeys belong to the address, so a move is a re-enrollment event. The command shows what the move affects and asks first. It takes and verifies an archive (the way back), then moves the installation to the new address and gives Convex the new token issuer. It removes the old address’s passkeys, ends every browser session, and prints one single-use link per person to enroll a new passkey on the same identity. CLI, agent, and machine credentials, OIDC bindings, Sync Targets, and webhooks keep working; only their configured server address changes. A move that stops resumes with anothervarlatch move, andvarlatch move --abandongives one up. The server records each move’s address and never removes passkeys twice. See Moving an installation to another address.varlatch admin reenrollissues those links at any time, for everyone (--all) or for named people (--identity), for example for someone who lost their passkey. A new link for a person revokes the unused one before it. Links expire after 24 hours by default (at most a week).varlatch setuprefuses apublicUrledited intovarlatch-install.json: rerunning setup with it skipped everything a move needs.- The sign-in and Security pages no longer send people to an organization admin for a recovery link: only whoever runs the server can issue one.
Upgrading
Section titled “Upgrading”Database schema: migration 26 (re-enrollment grants, and the address an installation last moved to).
- From 0.14.3, 0.14.2, 0.14.1, 0.14.0, 0.13.0, 0.12.0, or 0.11.0: download
varlatch-cli-0.15.0.cjsfrom thev0.15.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.15.0.cjs upgrade 0.15.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive while the installation keeps serving, applies migration 26, and completes only once the new release passes its health gate. Then replace the host CLI withvarlatch-cli-0.15.0.cjs, or runvarlatch self-update. - From 0.14.2 or older, the 0.14.3 notes below apply too:
.envmust setVARLATCH_PUBLIC_URL. From 0.13.0 or older, the upgrade also applies migrations 23 to 25, and the 0.14.0 notes below apply. - From 0.10.1 or older: follow the 0.11.0 instructions below with the 0.15.0 CLI and version.
- 0.15.0 restores everything 0.14.3 restores, and archives from 0.14.3.
0.14.3 (2026-10-06)
Section titled “0.14.3 (2026-10-06)”Coding agents
Section titled “Coding agents”- A refused tunnel no longer looks like a network failure. Inside an
agent-safe run, curl,
fetch, and most SDKs open aCONNECTtunnel, which the Broker refuses for an allowed destination. curl prints onlyCONNECT tunnel failed, response 502, and agents read that as the API being down. Now avarlatch run -- <command>inside the run ends, after such a refusal, with a note that names the destination and thevarlatch requestcommand to send instead. The refusal’s status line reads502 Tunnel refused by Varlatch, use varlatch request, whichcurl -vand Python’s errors show. The run’s own output names each refused destination and counts the refusals. The skill says what curl’s error means. Tunnels stay refused, and no value is shown.
Licensing
Section titled “Licensing”- Settings → Server has a Source code row that links to the source of
the release the server runs and names the licenses of its parts. If you
run a modified Varlatch for other people, the AGPL asks you to offer them
your modified source: point
SOURCE_REPOSITORYinapps/web/src/lib/source.tsat it. - The release images (
varlatchd,varlatch-web, andvarlatch-convex-deploy) carry the OCI labelsorg.opencontainers.image.licenses(AGPL-3.0-or-later AND Apache-2.0) andorg.opencontainers.image.source. Their third-party contents stay listed inTHIRD-PARTY-NOTICES.mdinside each image.
Documentation
Section titled “Documentation”- Getting started is new: one page from nothing
to
varlatch run, covering getting the CLI on any machine, running an installation withvarlatch setup, setting up a project, inviting your team, and connecting CI. The deployment guide no longer says it runs the Secret Plane only. - The documentation is now a website, docs.varlatch.com, built from the
same files: this repository’s
docs/, the deployment guide, the vocabulary, and this changelog, with search.apps/docsbuilds it, and the build fails on a link to a page or heading that does not exist. - Host requirements and the
configuration reference are new.
The first lists what a host needs, with the memory, processor, and disk
an installation was measured to use; the second, every setting of an
installation, what reads it, its default, and whether
varlatch setupwrites it. - Self-hosting instructions that did not work as written are corrected:
- The manual upgrade’s health check now uses the dashboard’s port,
127.0.0.1:8787/readyz. Installations set up withvarlatch setuppublish no fixed varlatchd port, solocalhost:8686failed there. - The Coolify guide gave the Root KEK mode 0600, which varlatchd’s own
user cannot read, so varlatchd did not start. Like
varlatch setup, it now uses mode 0644 inside a mode-0700 directory. The installation by hand does the same, and createsbackups/before Docker creates it as root, which madebackup createfail. - A changed
CONVEX_CLOUD_ORIGINtakesdocker compose up -d(on Coolify, a redeploy), not a restart, which keeps the old value.varlatch doctor’s remedy now says so. .env.examplelists every variable the Compose files read, and no longer suggests routing a domain at varlatchd’s port or publishing the ports on a LAN, where passkeys cannot work without HTTPS.
- The manual upgrade’s health check now uses the dashboard’s port,
- The Application Plane database no longer stores a new version of every dashboard read model (Mirror) each minute. varlatchd republishes every Mirror once a minute, and Convex stored each republish as a new document version even when nothing had changed, keeping superseded versions for at least its 14-day retention window: 3.3 GiB after 14 days on a small installation. An unchanged Mirror is now left as it is, which also stops waking every open dashboard each minute. This change does not remove the versions already stored.
- Dependency update for a published advisory:
@modelcontextprotocol/sdk1.31.0 (GHSA-6qxp-vccf-f47h, in the SDK’s OAuth client, which could send credentials to an authorization server the MCP server chose). Varlatch’s MCP server, bundled in the CLI, does not use that client.
Self-hosting
Section titled “Self-hosting”varlatch setupchecks Docker Compose before it changes anything. It needs Compose 2.24 or newer with every ingress (the Tailscale overlay uses!reset, which Compose 2.24 introduced), and stops with the version it found and the one it needs when Compose is missing, older, or reports no version it can read. Before, an older Compose failed halfway through setup, inside a Docker command.varlatch doctorreports the version as the advisory checkcompose.version; it never blocks the upgrade gate.- A
docker-compose.override.ymlnow applies with the public and tailnet ingress. Compose reads that file by itself only whileCOMPOSE_FILEis not set, and setup setsCOMPOSE_FILEfor those two, so the override file the deployment guide recommends was silently ignored there. Setup now lists it last inCOMPOSE_FILEwhen it exists, and only then, since Compose refuses a listed file that is missing;varlatch adoptdoes the same when it hands.envto setup. After you create or delete the file, runvarlatch setupagain.varlatch doctorwarns, as the advisory checkcompose.override, when the file exists andCOMPOSE_FILEleaves it out. If such an installation already has the file, its settings never applied: check that they are still what you want, then runvarlatch setupagain. See the deployment guide. - Compose refuses to start without
VARLATCH_PUBLIC_URL. Without it, varlatchd,convex-deploy, andvarlatchd admin mirror-synceach fell back to a different token issuer. Convex then rejected varlatchd’s tokens: the stack started, but the dashboard never updated andvarlatch doctorreportedmirror.catch-upas rejected. Every Compose file now requires the variable, and the three use the same issuer.varlatch setupalways writes it, so installations made with setup are not affected. If you wrote.envby hand without it,varlatch upgradestops before it changes anything and names the variable: set it to the address people open in the browser, then run the upgrade again.
Upgrading
Section titled “Upgrading”No database migration: 0.14.3 runs on 0.14.2’s schema (migration 25).
- First check that your
.envsetsVARLATCH_PUBLIC_URL. Installations made withvarlatch setuphave it. Without it, the upgrade stops before it changes anything; see Self-hosting above. - From 0.14.2, 0.14.1, 0.14.0, 0.13.0, 0.12.0, or 0.11.0: download
varlatch-cli-0.14.3.cjsfrom thev0.14.3release, check it againstSHA256SUMS, and runnode varlatch-cli-0.14.3.cjs upgrade 0.14.3 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. From 0.13.0 or older, it also applies migrations 23 to 25, and the 0.14.0 notes below apply. Then replace the host CLI withvarlatch-cli-0.14.3.cjs, or runvarlatch self-update. - From 0.10.1 or older: follow the 0.11.0 instructions below with the 0.14.3 CLI and version.
- Run
varlatch agents installagain in projects that use the installed skill orAGENTS.mdblock: the skill now explains the Broker’s tunnel refusal.agents install --checkreports drift until then. - 0.14.3 restores everything 0.14.2 restores, and archives from 0.14.2.
0.14.2 (2026-10-06)
Section titled “0.14.2 (2026-10-06)”Coding agents
Section titled “Coding agents”varlatch run --omit <NAME>(repeatable) now works in every run, not only with--agent-safe: the command does not get the item, neither from Varlatch nor as a copy inherited from your shell, and in a default run an omitted Secret is not fetched. An assisted run checks only the Secrets the command gets, so a provider-issued Secret shorter than 8 bytes that the command does not need no longer stops it: before, the only ways past that refusal were the human’s--allow-unmasked, a new value, or marking the item as not secret. A coding agent may add--omititself, but only when the command’s code or documentation shows that the command does not use the item; when it is not sure, it stops and asks. The exit-78 message offers this first, with the command to rerun, and says what the agent’s answer owes: the items it left out, and whether the task itself was done, since getting past the refusal does not show that it was. In assisted mode the run repeats that on stderr.--allow-unmaskedand--no-redactstay refused in assisted mode, and every other Secret is masked as before. A name that is neither stored in the environment nor in its Contract exits 64 before anything is disclosed or started. With--strict, omitting an item the Contract requires is a violation (78); with--export-context, the run context records the item as absent. An omitted Secret is not masked if the command reads it some other way. The skill and theAGENTS.mdblock say the same: runvarlatch agents installagain to update them. See assisted mode.- The opt-in guardrails’ hook (
varlatch agents hook) reads commands more accurately. It no longer denies a search pattern or a jq filter that starts with.env(grep -E "\.env|app",jq .environment): a name that is not.env,.envrc, or.env.<name>counts only as an existing file, and a backslash in double quotes is kept as the shell keeps it. It now denies a glob that matches a .env file (cat .e*), a recursive search that would print one (grep -r,rg --hidden,ag --hidden,git grep --no-indexor a tracked .env, and Claude Code’s Grep tool in content mode), unless the search leaves .env files out or, for the tools that honor it, git ignores them; and printing a variable insidevarlatch runwithechoorprintf, asprintenv NAMEalready was, except shell variables such asHOMEandPATH. The guardrails stay opt-in and are accident prevention, not a boundary.
varlatch sync checkcompares a platform’s values with the Environment’s without revealing them: it reads Coolify or Convex through the same adapters assync push, compares in memory, and prints one status per Contract key (match,differs,missing-on-platform,extra-on-platform,unreadable,absent), never a value or a hash. Coolify’s preview rows are ignored. Exit 0 in sync, 1 drift, 2 not everything checked;--jsonfor scripts. The platform is read first, and only the Secrets it also holds are disclosed, by name. Assisted mode allows it, a Secret too short to mask included, so it replaces printing the environment throughvarlatch runto compare it, which assisted mode masks. GitHub Actions secrets are write-only and are reported asunreadable. See sync check.
- Dependency updates for three published advisories:
smol-toml1.9.0 (GHSA-r4xh-jqrq-34v2, a quadratic-time parse; the CLI readsvarlatch.tomland coding-agent settings with it),proxy-addr2.0.8 (GHSA-jqcg-44mw-7w3h, through the MCP server’s dependencies, bundled in the CLI), andsource-map-js1.2.2 (GHSA-68fv-2mgg-jv7q, build and test tooling only, in both the pnpm and Convex npm lockfiles). varlatch agents install(andvarlatch init) no longer breaks a project whoseCLAUDE.mdis a symbolic link toAGENTS.md, as Astro’s template ships it (#93). The Claude Code adapter wrote its import line through the link, over the block it had just added, soAGENTS.mdlost its Varlatch block and imported itself;--checkthen never passed. Now aCLAUDE.md(or.claude/CLAUDE.md) that isAGENTS.mdneeds no adapter and the output says so, and a second install changes nothing. Running install again repairs a file an earlier release left this way, and--removegives back its original bytes. Nothing is written through a symbolic link any more: anAGENTS.mdthat links to a file in the project is resolved and that file edited, one that links outside the project stops the command (78), and any other linked file is left as it is with the edit printed. See coding agents.- The web console no longer shows healthy Sync Targets as failing. A pass
that finds the destination already current records
converged, and the console counted every result other thanokas a failure, so after the first repair pass every connection reported all of its targets failing. A target now fails only when a run failed or the target was stopped. A push whose values landed but whose redeploy did not now shows as a warning, “Redeploy failed”, on the target and on its connection.
Upgrading
Section titled “Upgrading”No database migration: 0.14.2 runs on 0.14.1’s schema (migration 25).
- From 0.14.1, 0.14.0, 0.13.0, 0.12.0, or 0.11.0: download
varlatch-cli-0.14.2.cjsfrom thev0.14.2release, check it againstSHA256SUMS, and runnode varlatch-cli-0.14.2.cjs upgrade 0.14.2 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. From 0.13.0 or older, it also applies migrations 23 to 25, and the 0.14.0 notes below apply. Then replace the host CLI withvarlatch-cli-0.14.2.cjs, or runvarlatch self-update. - From 0.10.1 or older: follow the 0.11.0 instructions below with the 0.14.2 CLI and version.
- Run
varlatch agents installagain in projects that use the installed skill orAGENTS.mdblock.agents install --checkreports drift until those instructions are updated. - Guardrails remain opt-in. The
.envintegration result covers the tested read paths, agent versions, models, and settings; it is not a boundary. - 0.14.2 restores everything 0.14.1 restores, and archives from 0.14.1.
0.14.1 (2026-10-05)
Section titled “0.14.1 (2026-10-05)”varlatch types --checkno longer reports a generated file as stale when the only difference is its Generator lines, the CLI release named in the header. A file another release generated is current when that release would produce exactly its bytes, so upgrading the CLI in CI no longer fails every generated file, andvarlatch typesleaves such a file unchanged. Any other difference, from the Contract or from a release that generates a different module, is still stale.
Release verification
Section titled “Release verification”- Verifying a release now covers a
release that is not signed: a release from the public repository is
always signed; a private repository’s release is signed only when the
repository opts in, and its notes then say it is not signed. For such a
release, a script checks every asset against
SHA256SUMSand the digests GitHub recorded, and checks the platforms, provenance, and SBOMs of the images the manifest pins. The guide also states what those checks cannot show: they do not prove that the release workflow produced the release. Missing signatures on a release you expect to be signed mean: stop. - A release that is not signed says so at the top of its release notes.
Upgrading
Section titled “Upgrading”No database migration: 0.14.1 runs on 0.14.0’s schema (migration 25).
- From 0.14.0, 0.13.0, 0.12.0, or 0.11.0: download
varlatch-cli-0.14.1.cjsfrom thev0.14.1release, check it againstSHA256SUMS, and runnode varlatch-cli-0.14.1.cjs upgrade 0.14.1 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. From 0.13.0 or older, it also applies migrations 23 to 25, and the 0.14.0 notes below apply. Then replace the host CLI withvarlatch-cli-0.14.1.cjs, or runvarlatch self-update. - From 0.10.1 or older: follow the 0.11.0 instructions below with the 0.14.1 CLI and version.
- 0.14.1 restores everything 0.14.0 restores, and archives from 0.14.0.
- Files generated by
varlatch typesneed no regeneration: an older release’s file stays current with the 0.14.1 CLI unless the module itself changed.
0.14.0 (2026-10-03)
Section titled “0.14.0 (2026-10-03)”Coding agents
Section titled “Coding agents”- Evaluated with Claude Code 2.1.278 and Codex CLI 0.159.2. On
2026-10-02, Varlatch’s agent evaluation of commit
b61a791passed all 36 cases, 18 for each coding agent (nine tasks, with and without the agents’ markers), against a local test server, without hooks. The result belongs to that commit: features added after it are tested separately. It is not a statement about other versions, other coding agents, or the same vendors’ editor or cloud agents; see coding agents. - Assisted mode for a coding agent driving the CLI with your credential
(Claude Code, Codex, Cursor, and others). Turn it on with
varlatch --assisted <command>in every command, orVARLATCH_ASSISTED=1. A coding agent’s own shell marker (CLAUDECODE,CODEX_THREAD_ID,CURSOR_AGENT,COPILOT_CLI,COPILOT_AGENT,GEMINI_CLI,OPENCODE,AGENT,AI_AGENT) turns it on as a backstop;VARLATCH_ASSISTED=0turns marker detection off but never overrides--assisted. It changes the CLI’s behaviour only, never authorization. See assisted mode. - In assisted mode,
varlatch runmasks Secrets in the command’s output by default, on a terminal too (the command then writes to pipes). The filter also holds inherited values under names Varlatch knows as Secrets. A Secret shorter than 8 bytes, which cannot be masked, stops the run with status 78 before the command starts, naming the item. - In assisted mode,
values setandvalues rotaterefuse a Secret’s value on the command line, storing nothing, and never prompt. - Behaviour change for agent sessions: a
varlatch runstarted by a coding agent that sets one of those markers now masks Secrets in its output, and may stop with status 78 on a Secret shorter than 8 bytes. SetVARLATCH_ASSISTED=0to keep the previous behaviour in such a shell. - The Varlatch skill for coding agents, built into the CLI: one skill in
the open Agent Skills format, plus a block for
AGENTS.md, telling a coding agent to usevarlatch --assisted, never read.envfiles or put a secret in a command, and hand the human the steps that are theirs. It names no coding agent’s tools. See coding agents. - New
varlatch agents installwrites the skill to.agents/skills/varlatch/and.claude/skills/varlatch/, the block toAGENTS.md(keeping everything outside its markers), and small adapters: an@AGENTS.mdimport in an existingCLAUDE.md,context.fileNamein.gemini/settings.json(only when the edit keeps the file’s formatting), andread:in.aider.conf.yml(only when its layout makes appending safe). Existing files keep every byte, and--removegives them back exactly as they were.--checkexits 1 when the files differ, for CI;--removetakes back what it wrote;--scope userinstalls the skill for every repository on the machine;--agent <name>creates an adapter’s file. - New
varlatch agents guide [topic]prints the skill or one of its references, for a coding agent without skill support. - From the first agent evaluation (it failed; see the coding-agents docs):
the skill now has the whole onboarding pattern in its main file
(
import <file> --contract --plain <non-secret> --delete-source, thencontract activate); requires the human’s approval, for the named item only, before an existing value is replaced or an item’s sensitivity changes; gives the human-terminal command for a missing value (varlatch values set <NAME> -e <environment>); shows--allow-unmaskedbefore--; and says, in the skill and the AGENTS.md block, that inside an agent-safe run Placeholders belong invarlatch --assisted requesttargets, with an example. - Behaviour change:
varlatch importtakes its options on either side of the filename (import --dry-run .envexited 64 although the skill showed it) and now refuses an unknown option, a missing option value, or a second filename with status 64; before, it ignored an option it did not know. - Behaviour change:
varlatch values setandvalues rotatecheck their command line strictly, in every mode, before anything is read, prompted for, or written. An unknown option, an option without its value, an option given twice (-eand--environmentcount as one), an extra argument, or a--gracethat is not a whole number exits 64 and stores nothing. Before, an unknown option was taken as the value: in a terminal,values set STRIPE_KEY --env productionstored the literal--envin the default environment. A value that begins with-now goes after--(values set OFFSET -- -1);--json, which these commands never supported, is refused.importalso counts-eand--environmentas one option. values setandvalues rotaterefusals in assisted mode (a value on the command line, or no value) now name the environment in every command they suggest, and put first the human-terminal command for a value from elsewhere:varlatch values set <ITEM> -e <environment>. Before, the suggested command had no-eand targeted the default environment.- The exit-78 message for a Secret too short to mask now says every remedy
is the human’s decision, for that item only (replacing the value
overwrites it), and shows the override as a command with the option
before
--. Both suggested commands name the refused run’s environment, and its server when overridden; the retry also keeps--strict,--allow-inherited, and earlier allowances. Before, a remedy for a production refusal replaced or showed the default environment’s value. - From the second agent evaluation (it failed; see the coding-agents docs):
the AGENTS.md block and the skill now say that a repository with
varlatch.tomlneeds novarlatch init; that a.envfile may hold values not yet in Varlatch or older copies, so names are compared first and an existing value is replaced only with the human’s approval for that item; thatvarlatch --assisted validate -e <environment> --jsonanswers whether an environment is ready (listing values does not check the Contract); that a run with no stored values gets only what it inherits; and that inside an agent-safe run only the listed Placeholders are safe to show. - Behaviour change: in assisted mode,
varlatch importrefuses (78, storing nothing and deleting nothing) to replace a value the target environment already has, unless that item is named with--replace <NAME>; a leftover file may be an older copy of a value rotated since. The dry run, in every mode, marks names the environment already has (existingin its JSON); an identical plain value, compared in its stored form, is not a replacement. A retry after a partial import needs the same approval for the values it now finds stored. An import outside assisted mode still replaces. - Inside an agent-safe run,
varlatch context --jsonlists the variables holding Placeholders (agentRun.placeholders, names only), and a nestedvarlatch runmasks the run’s own credentials (the agent-run credential, the Broker’s proxy credential) in its command’s output. - In assisted mode,
varlatch runsays on stderr, before the command starts, when no values are stored in Varlatch for the environment (the command gets only what it inherits), and when a.envfile exists in the project that the run does not read (only its existence is checked, never its content), with the value-free command to compare its names. varlatch initin a repository that is already set up still exits 1, and now says what to do next: compare a.envfile’s names, or validate an environment.- The skill tells a coding agent that status 69 can come from a sandbox blocking the connection, and to ask the human to allow it rather than work around it.
- From the third agent evaluation (it failed): the AGENTS.md block again
has the whole onboarding pattern (compare names first,
--contract,--plainonly for items the human confirmed are not secret,--delete-sourceonly once every value is stored, then activate the revision), tells the agent never to add--allow-unmaskedor--no-redact, and gives the human-terminal command for a value the human provides (varlatch values set <NAME> -e <environment>). - Behaviour change: assisted mode refuses
--allow-unmaskedand--no-redact(64, nothing started): showing a Secret is the human’s decision, in their own terminal. Outside assisted mode,varlatch run --allow-unmasked <NAME> -- <command>is that override: it shows only the named short Secrets and keeps every other known Secret, inherited ones included, masked; another short one still stops the run. Before,--allow-unmaskedapplied only in assisted mode. The option is checked strictly in every mode: a missing name or the--allow-unmasked=<NAME>spelling exits 64 before anything is fetched, never a run without masking. The exit-78 message and this refusal present every remedy as the human’s command for their own terminal (no--assisted), with the run’s environment, server, and startup options (--strict,--allow-inherited,--export-context), and say the agent stops and waits. - Behaviour change:
varlatch runchecks its own options, before--, strictly in every mode, an agent-safe run and a run nested inside one included: an unknown option (--allow-unmask PINgave a run that masked nothing), an option without its value, an option given twice (the repeatable ones excepted), or an argument before--exits 64 before anything is fetched or started. Before,runignored an option it did not know. The command’s arguments after--are passed unchanged. - Behaviour change: in assisted mode,
values setandvalues rotaterefuse (78, nothing stored) to replace an existing value unless the item is named with--replace <ITEM>. The check comes after the command line (value source,--generateform, a missing value: 64, no request) and before any value is read, prompted for, generated, or written. A value inherited from the parent environment counts as existing, and so does one whose existence cannot be checked.--replacerecords the override’s intent, not an approval. Outside assisted mode they replace as before. - Behaviour change: in assisted mode,
values deletedeletes only with the item named again,--confirm <ITEM>, for a plain value and a Secret alike; otherwise it exits 78 with no request and nothing deleted.--confirmnaming another item exits 64.--confirmrecords the deletion’s intent, not an approval.values deletenow checks its command line strictly in every mode (an unknown option such as--env, a missing value, a repeated option, or an extra argument: 64, no request); before,values delete X --env productiondeleted X from the default environment. Outside assisted mode a deletion needs no confirmation, as before. - Behaviour change: the MCP server’s
varlatch_delete_value(with--allow-writes) deletes only when itsconfirmargument repeats the item’s name; otherwise it returns an error and makes no request. - Behaviour change: the MCP server’s
varlatch_set_valuereplaces a plain value the environment already has, or inherits from its parent, only when itsreplaceargument repeats the item’s name; otherwise it returns an error and writes nothing, and unknown existence counts as existing. A new plain value needs noreplace; a Secret stays unwritable with or without it;replacenaming another item is refused before any request.replacerecords intent, not an approval. - From the fourth agent evaluation (it failed): the exit-78 rule is the
same everywhere (the CLI message, the AGENTS.md block, the skill and its
references): first stop and ask; after the human approves a remedy for
the named item and environment, the agent may carry it out with
--assisted. An approved new random value isvarlatch --assisted values set <NAME> -e <environment> --replace <NAME> --generate hex:32. The exit-78 message no longer prints a generated replacement without--assisted, says a provider’s credential is the human’s to enter, keeps showing a Secret unmasked as the human’s alone, and points to a new guide topic,varlatch agents guide contract, for marking an item as not secret (show the Contract, change only that item,contract push --file, activate; project-wide; in a new temporary directory, so no project file is overwritten; with the refused run’s--serveron every contract command, which the exit-78 message names). The usage now lists--serverforvarlatch contract. The instructions also say: generate a value only when the human asks for a new random one; never run a command printed for the human’s own terminal; quote a Placeholder variable in avarlatch requestheader with double quotes (single quotes send the literal$NAME). - From the fifth agent evaluation (it failed): the skill, its references,
and the AGENTS.md block tell a coding agent to work in the environment
the human named, or else the project’s default
(
varlatch --assisted context --jsonshows it), never another environment or server to get around missing configuration; and to create values only when the task asks for it (setting up, importing, a requested new value), in the environment it is for, or after the human approves that item there. Starting or checking an app never authorizes creating values. The CLI’s behaviour is unchanged. - New, opt-in
varlatch agents install --guardrails: for Claude Code,VARLATCH_ASSISTED=1, deny rules for.envfiles and the credential store, and aPreToolUsehook in.claude/settings.json; for Codex, a hook in.codex/hooks.jsonandVARLATCH_ASSISTEDin.codex/config.toml. Every hook runsvarlatch agents hook --format <claude|codex>, which denies reading.envfiles or the credential store, and printing the environment of avarlatch run. Accident prevention, not a boundary. See coding agents. - New, opt-in
varlatch agents install --mcpadds avarlatchserver runningvarlatch mcpto the project’s MCP files (.mcp.json,.cursor/mcp.json,.vscode/mcp.json,.gemini/settings.json,opencode.json,.codex/config.toml), for coding agents that should use MCP. See coding agents. - Behaviour change:
varlatch initnow also writes the agent files. Pass--no-agent-filesto skip them.initandagentsrefuse an unknown option, a missing option value, or an extra argument with status 64, before writing anything; before,initignored an option it did not know.
Sign-in
Section titled “Sign-in”- Device sign-in.
varlatch login --server <url> --startprints an address and a short code and exits. Open the address in a browser on any device, sign in with your passkey, enter the code, check who asks, and approve with your passkey again (or deny).varlatch login --server <url> --waitthen collects the credential and stores it like any login: 12 hours by default, up to 24 with--ttlwhen starting. It works from SSH sessions, containers, and hosts without a browser, and lets a coding agent start a sign-in and finish it after you approve, without a command that blocks. The code lasts 10 minutes and completes one sign-in.--waitwaits up to 60 seconds (--timeout, at most 600) and exits 75 when the sign-in is still waiting for approval, keeping it for the next--wait; it exits 77 when the sign-in was denied, expired, or already collected (naming that credential, to revoke).--jsonprints the address, the code, and the expiry, or the state. The sign-in’s private code lives only inpending-sign-ins.jsonin the CLI’s configuration directory, readable only by you, and is never printed. - Device sign-in runs only over HTTPS, or to a loopback address in local
development:
--startand--waitrefuse anhttp://server before sending anything (64), and follow no redirect. - In assisted mode,
varlatch --assisted login --server <url>with no sign-in method starts a device sign-in, since a coding agent cannot wait for the browser;--token-stdin,--token, and--oidcwork as before. The skill and theAGENTS.mdblock tell the agent to give you the address and the code, wait until you say you approved, and only then run--wait. - Behaviour change: inside an agent-safe run, every
varlatch login(in the browser,--token,--token-stdin,--oidc,--start,--wait) exits 64 before sending anything or touching the sign-in state. The run’s read access comes from--agent-metadata. - Behaviour change:
varlatch loginchecks its options strictly: an unknown option, or two sign-in methods, exit 64. Before, an unknown option was ignored. - Self-hosting: client addresses behind proxies. varlatchd now reads a
caller’s address from
X-Forwarded-For, but only when the request comes from a proxy named in the newVARLATCH_TRUSTED_PROXIES. It reads the header from the right, so an address a caller writes into it is never used. The Compose files set the variable to the dashboard’s nginx and their TLS proxy (Caddy, Tailscale serve, or Coolify’s Traefik), and the dashboard’s nginx now passes the header on. The per-client limits (600 requests a minute; device sign-in’s pending and wrong-code caps) and the device sign-in confirmation now see each caller’s own address instead of the proxy’s. Behind your own reverse proxy, add it to the variable; see “Client addresses behind proxies” ininfra/compose/README.md. - The agent evaluation of commit
b61a791(under Coding agents) predates device sign-in and does not cover it.
Dashboard
Section titled “Dashboard”- Redesigned dashboard. A new design system in both themes (Inter and
JetBrains Mono bundled), a collapsible sidebar, breadcrumbs on every page,
and keyboard shortcuts throughout:
/filters the current list, arrow keys and Enter move through and open rows,?lists every shortcut, andg p,g a,g c,g l,g snavigate. The command palette groups environments, projects, Config Items and pages, and runs actions. - The project page is a values grid: Config Items across environments, non-secret values in place, Secrets masked until an audited reveal, missing required values marked, and cells editable in place. Edits stay drafts until Review and Save, which saves one atomic change set per environment and keeps the checkbox for production.
- Each environment has its own page with an item panel (description, reveal, rotation, sync status and history), plus Integrations and Activity tabs. The project has Contract, Integrations and Activity tabs; a managed contract is edited like a spreadsheet and published from a diff.
- The audit log reads as sentences grouped by day, with names instead of ids, filters on the server, and a detail panel that explains decisions.
- The
/devicepage approves a CLI’s device sign-in: type the code the CLI shows (it is never taken from the link), see the address and client that asked and how long the credential lasts, then approve with your passkey or deny. - Access is organized as People, Machines, Roles and teams, Grants (written as sentences, built with a sentence builder) and Advanced. Pending invitations can be renewed or revoked, and organization roles are shown with the grants they carry.
- New account pages (profile, passkeys, sessions), organization settings with a rename, an installation backups page, and a branded passkey enrollment page.
- Confirmations, prompts and errors are in-app dialogs and toasts; the dashboard no longer uses the browser’s native dialogs.
- The MCP server ships inside the CLI as
varlatch mcp, in every release and under the CLI’s version. See the MCP server. - Removed: secret disclosure through MCP.
varlatch_disclose_secretsis gone, and--allow-discloseandVARLATCH_MCP_ALLOW_DISCLOSEare refused with status 64. No MCP host can keep a tool result out of the model’s context. varlatch_set_valuerefuses a Secret’s value (a sensitive item, an item outside the Contract, or any item when the Contract cannot be read) and writes nothing.- Inside an agent-safe run the server uses only the agent-run credential,
never the operator’s stored one, and refuses
--allow-writes. - The old
varlatch-mcpentry point runs the same code with a deprecation notice; replace it withvarlatch mcp.
--jsonon the read commands that lacked it:values list,validate,org list,project list,identity list,credential list,audit list, andtailnet requirements, pluscontract pushandimport. Each prints one JSON document with"version": 1; no command prints a value. See scripting.- Changed exit statuses: a wrong command line now exits 64 (it exited
1), not being signed in or a denied request 77, and a server that cannot
be reached, or is in maintenance or overloaded, 69 instead of 1 or a
stack trace. Kept as before:
validate1 and 2,scan1 (every failure) and 2,types --check1, strict startup 78, and the command’s own status fromvarlatch run. A script that checks for exactly 1 on these errors needs updating. A malformed command is 64 whether or not a credential is stored, and a proxy’s HTML error page keeps its HTTP status (a 502 is 69) instead of ending in a stack trace. varlatch --help,-h,help, and<command> --help(orhelp <command>) print on stdout and exit 0; before,--helpexited 1. An unknown command exits 64 with the usage on stderr.
Values
Section titled “Values”-
varlatch login --token-stdinreads the credential from a pipe or file, so it never appears on a command line, and refuses a terminal. The “not authenticated” message now suggests browser sign-in or--token-stdininstead of--token <credential>. -
values setandvalues rotatetake the value from--stdin,--from-file <path>, or--generate hex|base64|base64url:<bytes>oralnum:<characters>(from the system’s secure random source, never shown), or, with no value in a terminal, from a hidden prompt, where Backspace removes a whole character and arrow keys are ignored. The value then never appears on a command line. -
New
varlatch import <file>stores a dotenv file’s values without printing them: names, counts, inferred types, and sensitivity only.--dry-runshows the plan (and lists names even outside a repository),--contractadds the file’s new items to a Contract revision as Secrets unless--plain <NAME>, and--delete-sourcedeletes the file once every value is stored. A file that is not valid UTF-8 is refused before anything is stored or deleted. See importing.
Agent-safe runs
Section titled “Agent-safe runs”-
New
varlatch request, a curl-like client for the Agent inside an agent-safe run:-X,-H,-d,--json,-o,-i. It sends each request to the run’s Broker in the form substitution needs, so the Secret is substituted at its targets, TLS is originated by the Broker, and the response is scrubbed. Clients that tunnel HTTPS with CONNECT (curl,fetch, most SDKs) get a 502, whose explanation now namesvarlatch request. It refuses outside an agent-safe run. See agent-safe runs. -
The Agent gets its own empty configuration directory (
VARLATCH_CONFIG_DIR, removed when the run ends) and the run’s identifier (VARLATCH_AGENT_RUN). Avarlatchcommand the Agent starts never reads or writes the operator’s credential store: before, a nestedvarlatch run -- printenv <SECRET>found the operator’s stored credential and printed the Secret. A nestedvarlatch runnow starts its command with the run’s environment, Placeholders included, and discloses nothing; other commands use the--agent-metadatacredential or stop.
- Invitations can be listed and revoked (capability
invitations.manage).GET /v1/organizations/{org}/invitationslists the pending ones, newest first;?status=alladds consumed, expired, and revoked ones. It never returns a token or anything derived from one.DELETE /v1/organizations/{org}/invitations/{invitation}revokes a pending invitation: its link stops working at once, also for an enrollment already started with it, and the audit eventinvitation.revokedrecords it. A consumed, expired, or already revoked invitation is refused withVERSION_CONFLICT, its status in the error’s details. Both needidentity.manage, like creating an invitation. Creating one now also returns itsid, whichinvitation.issuedandinvitation.acceptedrecord. SDK:listInvitations(),revokeInvitation(). Database migration 23 adds the revocation and the invitation’s creator (unknown for invitations created before it). - An organization’s display name can now be changed:
PATCH /v1/organizations/{org}with{"name": ...}(capabilityorganizations.rename), trimmed, 1 to 200 characters. It needsorganization.manage. The slug never changes. The audit eventorganization.renamedrecords the previous and the new name. SDK:renameOrganization(). - The audit listing and the NDJSON export filter on the server (capability
audit.filters):decision(allow,deny,info),eventType(an exact type, or a prefix such asvalue.*),actorIdentityId,projectId,environmentId,item(a Config Item named in the event’s resource, or in the item list of a disclosure or Capability event), andsince(inclusive) anduntil(exclusive) as RFC 3339 timestamps. All given filters must match. Cursors work as before, newest first by occurrence time and event ID; pass the same filters with each cursor. A malformed, empty, or repeated filter is refused withVALIDATION_FAILED. Who may read the audit log does not change. SDK:listAuditEvents()andexportAuditEventsNdjson()take the filters. - Credentials carry a readable
clientlabel, so you can tell your sessions apart: a short summary of the client that requested a browser session or a CLI login, such asFirefox on Linuxorvarlatch CLI 0.14.0 on Linux. Only the summary is stored, never the User-Agent itself, and it names no browser or system version. It isnullwhen the client is not recognized, for every other credential kind, and for credentials issued before this release.GET /v1/me/credentialsand the identity credential listing return it.varlatch loginnow sendsvarlatch-cli/<version> (<platform>; <arch>)as its User-Agent when it exchanges the browser session; the SDK takes auserAgentoption. Database migration 24 adds the column. - Device sign-in (capability
auth.device, advertised when the public URL is HTTPS or a loopback address). For the CLI, unauthenticated:POST /v1/auth/devicestarts a sign-in (a private device code, a user code, the verification address), andPOST /v1/auth/device/tokenpolls it:AUTHORIZATION_PENDING(428),SLOW_DOWN(429, the interval grows by 5 seconds),ACCESS_DENIED(403),EXPIRED(410), the CLI credential once (201), thenCONSUMED(410, with the credential’s id, never its token). For the dashboard, with a person’s browser session bearer:POST /v1/auth/device/lookupreturns the pending sign-in a typed code belongs to and a fresh passkey challenge, bound to that sign-in, the person, and their session, single use;POST /v1/auth/device/approvedenies, or approves with a passkey assertion over that challenge. Wrong codes are limited per person (5), per address (20), and in total (100) every 10 minutes, in the database, so a new session or a restart does not reset them; at most 10 sign-ins may be pending per address and 1,000 in total. Audit events:authentication.device_requested,_approved,_denied,_collected,_code_rejected, and_code_locked. SDK:startDeviceSignIn(),pollDeviceSignIn()(one request, no redirect followed),lookupDeviceSignIn(),decideDeviceSignIn(). Database migration 25 adds the sign-ins, their challenges, the attempt counters, and the session a browser bearer was minted from.
-
An open environment page no longer re-reads its values every two seconds. Each read is audited, and the read’s own audit event used to count as a change to the values, so the page fetched them again, recorded another read, and so on: the audit log filled with
value.disclosedevents and busy dashboards could reach the request limit. Reads, validations, denials and capability use now still refresh the audit log but no longer count as changes. -
An agent-safe run’s Broker no longer stops when a client resets its connection after a refused
CONNECT(407 without the proxy credential, 502 for an allowed destination, 403 with--agent-network strict). curl does this after the 502; the unhandled socket error ended the Broker, and with it every tunnel the Agent’s own traffic used. The refusals are unchanged. -
Output redaction, response scrubbing, and
varlatch scanno longer fail when a stored Secret holds an unpaired UTF-16 surrogate, which the API accepts and UTF-8 cannot carry: such a value is matched as a command receives it, with U+FFFD in place of the surrogate. Before, building the matcher threw, sovarlatch run --redactstopped before starting the command.
Upgrading
Section titled “Upgrading”Database schema: migrations 23, 24, and 25 (invitation revocation, the credentials’ client label, and device sign-in).
- From 0.13.0, 0.12.0, or 0.11.0: download
varlatch-cli-0.14.0.cjsfrom thev0.14.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.14.0.cjs upgrade 0.14.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive while the installation keeps serving, applies migrations 23 to 25, and completes only once the new release passes its health gate. Then replace the host CLI withvarlatch-cli-0.14.0.cjs, or runvarlatch self-update. - From 0.10.1 or older: follow the 0.11.0 instructions below with the 0.14.0 CLI and version.
- 0.14.0 restores everything 0.13.0 restores, and archives from 0.13.0.
- Client addresses behind proxies. The Compose files now set
VARLATCH_TRUSTED_PROXIESfor the dashboard’s nginx and the TLS proxy in front of it. If your own reverse proxy sits in front of the dashboard, add it to the variable in.env; see “Client addresses behind proxies” ininfra/compose/README.md. On Coolify, check varlatchd’s log after the upgrade fortrusted proxy coolify-proxy is <address>. If it says the name does not resolve, set the variable asinfra/coolify/README.mddescribes. Until then, every public caller counts as one client. - Scripts and coding agents that drive the CLI: see the behaviour changes
above. Exit statuses are now distinct (64, 69, 77, and 75 for
login --wait).varlatch loginchecks its options strictly and refuses every method inside an agent-safe run. A coding agent’s own shell marker turns assisted mode on.
0.13.0 (2026-09-30)
Section titled “0.13.0 (2026-09-30)”Projects
Section titled “Projects”- A project’s display name can now be changed:
varlatch project rename <slug> "<new name>", Rename in the project’s menu on the dashboard, orPATCH /v1/organizations/{org}/projects/{project}(capabilityprojects.rename). It needsproject.manage. The slug, which the CLI, URLs, contracts and grants use, never changes. The audit eventproject.renamedrecords the previous and the new name.
Contracts
Section titled “Contracts”- A new item type,
integer, holds whole numbers: ports, counts, sizes. It accepts an optional-and ASCII digits only, so3.0,3.5,+1, and1e3are invalid (“must be a whole number”), within the same bound asnumber(2^53 - 1). Declare it with@type=integerin.env.schema, or pick it on the dashboard.numberdoes not change: existing items keep accepting fractions. integercomes with Contract Semantics version 3, which is version 2 plus the new type. A project’s first revision now gets version 3. Existing Contracts keep their version until you move them.- An
integeritem in a Contract at version 1 or 2 is refused.varlatch contract pushsays so before sending anything and names the fix:--semantics latest, or the dashboard’s move action. - On the dashboard, a Contract on older rules offers Move to the newest rules. It creates a revision with the same items at the newest version and shows the difference and what changes, and you activate it in a separate step. For a Git-managed Contract, later pushes from the repository keep the new version.
- Generated types: an
integeris anumberin TypeScript and anintin Python.
- Generated Python configuration (
varlatch types) no longer fails on a whole number written with more than 4300 digits, such as1after 4300 leading zeros. Python refused to convert that much text to anint(ValueError), while the server and the TypeScript module accept it as1. Anumberorintegeritem now converts the same way in all three. Regenerate Python type files to pick up the fix.
Upgrading
Section titled “Upgrading”No database migration: 0.13.0 uses migration 22, like 0.12.0 and 0.11.0.
- From 0.12.0 or 0.11.0: download
varlatch-cli-0.13.0.cjsfrom thev0.13.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.13.0.cjs upgrade 0.13.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.13.0.cjs, or runvarlatch self-update. - From 0.10.1 or older: follow the 0.11.0 instructions below with the 0.13.0 CLI and version.
- 0.13.0 restores everything 0.12.0 restores, and archives from 0.12.0.
- Moving a project to Contract Semantics version 3 is a coordinated step.
Regenerate type files with
varlatch types:--checkfails until you do. Upgrade every CLI that runsvarlatch run --strictfor the project, CI included: an older CLI refuses a version 3 Contract. Nothing changes for a project until you move it.
0.12.0 (2026-09-29)
Section titled “0.12.0 (2026-09-29)”Type generation
Section titled “Type generation”varlatch types --out app/varlatch_config.pygenerates typed configuration for Python: one module, for Python 3.10 or later and the standard library only, with a frozenConfigdataclass,load_config(), and a lazily loadedconfig. It checks and converts values with the same rules as the TypeScript module and the server: a whole number is anint, a number with a fraction afloat, and a boolean abool. Sensitive items are left out ofrepr(). A URL with an internationalized host name needs the optionalada-urlpackage to be checked, and is reported, never accepted unchecked, without it. See Type generation.- Python 3.10 is supported until the first release after 31 October 2026, when its upstream support has ended; that release requires Python 3.11.
- On Coolify,
varlatch adoptcan now move Convex’s trust tovarlatchd. Coolify had put the Convex backend on another network than the namevarlatchd, so the step refused to change anything. The Coolify Compose file now puts Convex on the stack’s default network, as it already did for the dashboard. varlatch doctorno longer reports the Convex supervisor as stale after every Coolify deploy. It compared times, and Coolify rewrites the supervisor file, unchanged, after starting the containers. The supervisor now records a hash of the file it loaded, andvarlatch doctorandvarlatch upgradecompare content. A supervisor from an older release is still judged by time.- After a restore, the dashboard and the other remaining services start without waiting for varlatchd’s Docker health check. On a host where that check kept failing, a restore waited up to 10 minutes first.
Upgrading
Section titled “Upgrading”No database migration: 0.12.0 uses migration 22, like 0.11.0. Python generation is entirely in the CLI, so the server does not need to be upgraded for it.
- From 0.11.0: download
varlatch-cli-0.12.0.cjsfrom thev0.12.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.12.0.cjs upgrade 0.12.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.12.0.cjs, or runvarlatch self-update. - From 0.10.0 or 0.10.1, or older: follow the 0.11.0 instructions below with the 0.12.0 CLI and version.
- 0.12.0 restores everything 0.11.0 restores, and archives from 0.11.0.
0.11.0 (2026-09-28)
Section titled “0.11.0 (2026-09-28)”Contracts
Section titled “Contracts”-
.env.schemaconditions name your Varlatch environments directly:@required=env(production, staging)requires an item in those environments, and@required=tier(production)in every environment of a tier. Names are resolved to environment IDs when you push, and an unknown name fails the push and lists the environments that exist. A pushed revision keeps the IDs: renaming or deleting an environment later never changes it. A later push resolves the names again, so if a name was reused by a new environment, that push selects the new one. -
forEnv(...)is no longer accepted in.env.schema. The parse error names the replacement:@required=forEnv(prod)becomes@required=env(production), using the project’s environment names, or@required=tier(production). -
A comment after a value is now a comment:
PORT=8080 # listen portgives the default8080. Before, the comment became part of the default. A decorator after a value, text after a quoted value, and an unterminated quote are now errors instead of being read into the default. A#with no whitespace before it, or inside quotes, is still part of the value. -
The environment-name mapping is removed. It only translated
forEnv(...)names, which schemas no longer use. Removed with it:- the
varlatch varlock-mappingcommand; - the mapping card on the dashboard’s contract page;
- the
GET,PUT, andDELETEendpoints under/v1/organizations/{org}/projects/{project}/varlock-mapping, which now answer404; - the SDK methods
getVarlockMapping,setVarlockMapping, andremoveVarlockMapping.
Deleting an environment is no longer blocked by a mapping entry. It is still blocked while the active contract references the environment.
- the
-
Contract revisions record their Contract Semantics version: the rules for requiredness, validation, and conversion they are evaluated with. Every existing revision is version 1 and keeps its content hash. A new project’s first revision gets version 2, which adds conversion to typed values and bounds numbers: a number whose magnitude exceeds 9007199254740991 (2^53 - 1) is invalid. A push or edit keeps the active revision’s version, and
varlatch contract push --semantics latestmoves to the newest. Contract revisions in the API carrysemanticsVersion, and/v1/metalistssemanticsVersions. See Contract Semantics. -
The dashboard’s type help now matches the rules. It said booleans accept
yesandno, which validation never accepted. -
Validation checks values as
varlatch rundelivers them, with${NAME}references expanded. Before, it checked the stored text:${BASE}0was reported as an invalid number although it is delivered as8080, and a value whose reference stays literal when delivered passed. Such a value is now reported asunresolved, andvarlatch validateexits 1, or 2 when only the caller’s missing read access to non-sensitive values keeps it literal. Values read only to expand a reference are audited before they are decrypted.
Security
Section titled “Security”- Every retrieval (effective configuration, disclosure, validation, and Capability exercise) reads everything from one read-only database snapshot: the organization, project, and environment, authorization inputs, the active contract revision, values, and the ciphertext it may decrypt. A write, rotation, contract activation, or Grant change that commits while a request is in flight is invisible to that request, never half-applied.
- Each decryption follows an audit commit that names its exact version, and
a failed commit stops the request. Values decrypted only to expand
${NAME}references used to be audited after they were decrypted. They are now audited before, one reference level at a time. - Authorization is effective at the snapshot boundary: a revocation that commits after a request’s snapshot began does not affect that request. The next request sees it.
- Server-side sync delivery audits before it decrypts. Each reconcile that
decrypts values (on a trigger, or the six-hourly repair pass) first
commits a new
sync.values_decryptedevent naming the exact versions, even when it then pushes nothing, and stops if that commit fails. The existingsync.push_attemptedevent still precedes every push. - The agent-safe Broker substitutes a Secret only at the targets you name, and only there. Before, it replaced a Placeholder wherever it appeared in the headers or a text body sent to an allowed destination, so an Agent could have it write a Secret into a field that destination stores or publishes. varlatchd records the targets on the Capability when it issues it, and the Broker enforces the recorded ones, so the Agent cannot add or widen one. Anything unclear where a Secret may go blocks the request, and a failed request sends the destination nothing. See Agent-safe runs.
- The Broker scrubs responses to the requests it substituted into: a Secret
the destination echoes back, in its status line, headers, or body, as
written, JSON-escaped, percent-encoded, or base64-encoded, reaches the
Agent as its Placeholder.
gzip,deflate, andbrresponses are decoded to do so. This is a second line of defense with stated gaps, not a guarantee; see Agent-safe runs. - An agent-safe run removes stored Secrets and Contract Secrets from the environment the Agent inherits from your shell. Before, a Secret already set in your shell reached the Agent as plaintext.
- A Capability exercise returns only the Secrets the request places. A Secret it references is decrypted to expand the placed value, after an audit event naming its version, and is not returned.
Retrieval
Section titled “Retrieval”- Strict retrieval:
POST /v1/organizations/{org}/projects/{project}/environments/{environment}/retrievalswith{"mode": "strict"}returns, from one snapshot and in one request, every value the caller may receive, the state manifest and itsstateDigest, the caller view, the active contract (withcontract.read), and the validation of exactly the values returned. Each class of value is authorized separately: withoutsecret.reveal, the non-sensitive values still arrive and the Secrets are reported as withheld. A failure after the snapshot returns an error and no values. SDK:strictRetrieval()./v1/metalistsretrieval.strict. - Effective configuration responses, and disclosure responses for callers
who can read metadata, carry the state manifest,
stateDigest, and a caller view (retrieval.manifest). The manifest lists the environment, the active contract revision and its semantics version, and every item’s source and version IDs. It holds identifiers only, never values or anything derived from them, and its digest is the same for every caller looking at the same state. The caller view lists what this caller was not given and which references stayed literal for it.
- A disclosure request may declare
"purpose": "scan". The disclosure’ssecret.disclosedaudit event, and anyvalue.disclosedevent for a reference it expands, record it aspurpose. Only listed purposes are accepted; anything else is refused withVALIDATION_FAILEDbefore anything is audited or decrypted. A purpose grants nothing, and without one nothing changes. SDK:discloseSecrets()takespurpose./v1/metalistssecrets.disclosure-purpose. - Strict retrieval takes
{"mode": "preflight"}(capabilityretrieval.preflight): the non-sensitive values and, for a caller withsecret.reveal, a verdict per Secret, never a Secret value. Its responses carrystateDigests, one digest per category of the state. - Capability issuance accepts a
precondition(the state a preflight saw). When the state changed, it refuses with the new error codeSTATE_CHANGED(HTTP 409), naming only the categories that changed; on a match its response addspreflightItems: per item, whether it is stored and whether the Agent holdssecret.use. - Capability issuance requires
targets: per item, one to four ofheader:<name>,query:<name>,json:<pointer>, andform:<name>. Transport-owned headers such asHost,Content-Length, andContent-Typeare refused. The issuance response, every exercise response, and thecapability.issuedaudit event carry the targets. - A Capability exercise requires
placements, the item and target of each substitution the Broker makes, and returns only those items. A placement the Capability does not hold is denied with the reasonplacement-not-targeted, andcapability.exercisedrecords the placements./v1/metalistscapabilities.targets. GET /v1/organizations/{org}/projects/{project}/contract/revisions/{revision}returns one stored Contract revision, active or not, with its content hash and semantics version. It needscontract.read. SDK:getContractRevision()./v1/metalistscontracts.revision-by-id.
varlatch self-updatereplaces the CLI with a release’s single-file build, as installing it by hand does: it downloadsvarlatch-cli-<version>.cjsandSHA256SUMS, checks the signature onSHA256SUMSwith cosign when the release is signed, checks the file against it, shows what it verified, and asks before replacing the file it runs from.--checkonly reports whether a newer release exists (--jsonfor scripts). It refuses a CLI run from a source checkout, and never installs an older release. With--yes, a release whose signature was not checked needs--allow-unverifiedas well. While the repository is private, setGITHUB_TOKENorGH_TOKEN. An installation on the same host still upgrades withvarlatch upgrade.varlatch run --strictstarts the command only if the environment it would receive satisfies the active Contract. It retrieves everything in one request, applies Contract defaults only to items with no stored value (never to withheld ones), takes values from your shell only for names given with--allow-inherited NAME, and validates every value exactly as the command will receive it. Any violation (missing, withheld, parent-only, invalid, or a${NAME}reference left literal, and also no active Contract, nocontract.read, or an unsupported semantics version) starts nothing and exits 78, listing every violation by name, never by value. The command receivesVARLATCH_RUN_CONTEXT, which records what the server did and how each item was delivered. See Strict startup.varlatch run --strict --agent-safeapplies the same checks while the Agent receives Placeholders for Secrets. The operator’s preflight validates each Secret without returning it (it needssecret.reveal), the Broker’s Capability is issued against the state the preflight saw, and the Agent starts only if the Agent holdssecret.use, every Secret is valid, and no Contract Secret would come from your shell.--allow-inheritedcannot name a Secret in an agent-safe run.varlatch run --redactmasks the Secrets delivered to the command in its stdout and stderr, which become pipes. Each occurrence, as written, JSON-escaped, percent-encoded, or in base64 or base64url, becomes[REDACTED:<NAME>], and overlapping values become one marker. Output is matched as bytes, so binary output passes through unchanged unless it contains a Secret. Bytes that could be the start of a Secret wait for the command’s next write and are never released on a timer: they are released unchanged when the output ends, and discarded if the run is interrupted. Values shorter than 8 bytes are not masked, and the run names them. stdin, signals, and the exit code pass through; the relative order of stdout and stderr is not kept. It works with--strict, refuses to start when stdout or stderr is a terminal, and cannot be combined with--agent-safe, whose Agent receives Placeholders, not Secrets. See Output redaction.varlatch scan --stagedchecks what the next commit records, andvarlatch scan <path>...checks build output, for the Secrets of the selected environment that your identity may retrieve, at their current and retiring versions. The values come from one disclosure, audited withpurpose: "scan", and stay in memory. The Git index is read, not the working tree, so a Secret staged and then edited or deleted is still found. Files are scanned as bytes, across line breaks, in the same encoded forms that--redactmasks. Each finding names the file, line, column, item, version, and form, never the value or the line. A file over the size bounds, or one that cannot be read, is listed as not scanned, never as clean. The exit status is 1 for findings and 2 when some files were not scanned.varlatch:allow NAMEmarkers and a baseline file of paths, items, and version IDs accept known occurrences, andvarlatch scan --install-hookinstalls a pre-commit hook that runs the scan. See Secret scanning.varlatch run --agent-safetakes--target NAME=kind:location(repeatable) and--omit NAME. Every stored Secret needs one or the other. The Agent’s environment gainsNODE_USE_ENV_PROXY=1, so Node’sfetchgoes through the Broker, andNO_PROXYandno_proxyeach gain the Broker’s own address. The run reports blocked requests, stray Placeholders, and the inherited Secrets it removed, never values.varlatch types --out <file.ts>generates one TypeScript module from the active Contract Revision, or from--revision <id>: a type for every Contract item, a type of the non-sensitive items, and the Typed Accessor, a small runtime that readsprocess.env, converts values with the revision’s Contract Semantics, and throws one error that lists every problem by item name, never by value. The module is self-contained, so the application needs nothing else. Generation needscontract.read, makes one request, fetches no values, rewrites the file only when it changes, and never writes through a symbolic link.--checkexits 1 when the file is stale, for CI. A revision at Contract Semantics version 1 is refused, because version 1 defines no conversion. See Type generation.varlatch run --export-contextgives the commandVARLATCH_RUN_CONTEXTfrom a default run, withmode: "exported", so the Typed Accessor can check requiredness for the environment and tell withheld items from absent ones. The run is otherwise unchanged. It needscontract.readand an active Contract, and fetches the Contract Revision before any Secret is disclosed. The context always describes the values the command receives: the run checks that the configuration and the disclosure of its Secrets saw the same state (the samestateDigest), and when a write, rotation, deletion, or Contract activation lands between the two requests it makes both again, up to three times in all, each time an audited disclosure. If they never agree, it starts nothing and exits 1. A run without the flag makes the same two requests as before and checks nothing.- When a signal ends the command,
varlatch runexits with 128 plus the signal’s number, as a shell reports it, in every mode: 130 for SIGINT (Ctrl-C), 129 for SIGHUP, 131 for SIGQUIT, 139 for SIGSEGV. Before, every signal except SIGKILL gave 143, the status for SIGTERM, so a run interrupted with Ctrl-C looked like a run that was stopped. varlatch runforwards SIGHUP, SIGQUIT, SIGUSR1, and SIGUSR2 to the command, as well as SIGINT and SIGTERM, and keeps running until the command ends. A service manager can now reload a service it starts throughvarlatch run(ExecReload=kill -HUP $MAINPID), and a log rotation signal reaches the service. On Windows only SIGINT and SIGTERM are forwarded.
Upgrading
Section titled “Upgrading”Database schema: migrations 21 and 22.
- From 0.10.0 or 0.10.1: download
varlatch-cli-0.11.0.cjsfrom thev0.11.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.11.0.cjs upgrade 0.11.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive while the installation keeps serving, applies migrations 21 and 22, and completes only once the new release passes its health gate. - From 0.8.0 or 0.9.0: the same command. As for 0.10.0, the archive is taken with the installation briefly paused, so plan a short maintenance window. From the published 0.7.0, the offline path described under 0.8.0 still applies with this CLI.
- Then replace the host CLI with
varlatch-cli-0.11.0.cjs. Agent-safe runs need the 0.11.0 CLI and server together (see below). - 0.11.0 restores archives from 0.7.0 (migrations 16 and 18), 0.8.0 (migrations 18 and 19), 0.9.0 (migrations 19 and 20), 0.10.0 and 0.10.1 (migration 20), and builds made between 0.10.1 and 0.11.0 (migrations 21 and 22).
The changes below need attention when you upgrade.
-
varlatch runexits 130, not 143, when interrupted with Ctrl-C. Any signal that ends the command now gives 128 plus its number. SIGTERM still gives 143 and SIGKILL 137. A script that looks for 143 to detect an interrupted run should look for 130, or for any status above 128. -
SIGHUP, SIGQUIT, SIGUSR1, and SIGUSR2 sent to
varlatch runreach the command. Before, they endedvarlatch runitself and left the command running without it. A command that does not handle one of them now ends by it, with 128 plus its number. -
Agent-safe runs need targets. A run in which a stored Secret has neither
--targetnor--omitdoes not start; add--target NAME=header:authorization(orquery:,json:,form:) for each Secret the Agent uses, and--omit NAMEfor the rest. Requests that relied on substitution into atext/*body, or into a header not known in advance, have no replacement. -
Agent-safe runs need a 0.11.0 server and CLI together. varlatchd refuses to issue a Capability without targets, naming CLI 0.11.0, and a 0.11.0 CLI refuses an older server. A Capability issued before the upgrade has no targets and is refused at its next exercise with the reason
capability-without-targets; restart the agent-safe run with the new CLI. Revocation and expiry work as before. The CLI refuses an older server for every agent-safe run, including one with no Secrets, because removing inherited Secrets relies on the server’s account of the active Contract. -
--targetand--omitmust name Secrets stored in the environment. A name that is unknown, not stored there, or not a Secret is a usage error, and nothing starts. -
A Secret with a
json:orform:target constrains every request that carries its Placeholder: if such a request has a body, it must be a valid body of that kind (a matchingContent-Type, UTF-8, noContent-Encoding), even when the Placeholder is only in a header. Otherwise the Broker answers403. Give body targets only to Secrets the Agent sends in bodies. -
With an active Contract, an agent-safe run needs
contract.read, to know which inherited names are Secrets. Without it the run does not start. -
Responses to substituted requests change on the wire: the request asks for identity content without
Range; a response that is coded, has no length, or is over 2 MiB arrives chunked and uncoded, without validators or digests; a coding other thangzip,deflate, orbris a502. A substituted response is cut off after 64 MiB of decoded content or 120 seconds without data from the destination, so long-polling and server-sent events streams need a heartbeat more often than that. -
A Node Agent that set
HTTPS_PROXYand usedfetchfor an allowed destination now gets the Broker’sCONNECTrefusal instead of reaching the destination directly with the Placeholder. Send absolute-URI requests to the Broker instead. With--agent-network strict, Node’sfetchto other destinations is now blocked like other traffic, except for hosts your shell’sNO_PROXYorno_proxyexempts: those exemptions are kept and still bypass the Broker. The run adds only the Broker’s own address, and a spelling your shell did not set becomes that address alone. -
Every
varlatch run, including a default one, now removes aVARLATCH_RUN_CONTEXTinherited from an outer run. This is the one change to a default run. The name is reserved: the server refuses a Contract item or a stored value calledVARLATCH_RUN_CONTEXT. -
A disclosure or Capability exercise whose references are nested now records one
value.disclosedevent withmode: "reference-expansion"per reference level, instead of one for all levels. -
The upgrade drops existing environment-name mapping entries (a new schema migration). Contract revisions are unchanged: they store environment IDs, not names. Audit history keeps its
contract.varlock_mapping_setandcontract.varlock_mapping_removedevents. Backups taken with 0.10.0 or 0.10.1 restore into this release and are migrated forward. -
Existing projects keep version 1 rules. To adopt version 2, push with
varlatch contract push --semantics latestand review the activation, which shows the version change. -
Validation results can change for values that contain references: they are now checked as delivered, and a reference that stays literal makes the report invalid. The validation report gains an
unresolvedlist. -
A
.env.schemawith comments after values gives different defaults when pushed with this release’s CLI. Review the activation’s changes before activating. A file that usesforEnv(...)must switch toenv(...)ortier(...)first.
0.10.1 (2026-09-27)
Section titled “0.10.1 (2026-09-27)”- A restore now leaves the whole installation running. Before, it started
only the database, varlatchd, and Convex, so after restoring onto a stopped
or new host the dashboard and the ingress proxy stayed down until you ran
docker compose up -d, andvarlatch doctor --gatefailed. When restore finishes it now points you tovarlatch doctorand a fresh archive. On a slow host it no longer reports that not every service started when varlatchd’s health check is still recovering from the restore. - Installations that use the tailnet ingress with secrets in files no longer
print
The "VARLATCH_RUNTIME_PASSWORD" variable is not seton every Compose command. varlatch upgrade --release-dirno longer says it is fetching the release from GitHub. It reads only the files in that directory.varlatch upgradenow runs the new release’s own upgrade gate when Docker runs in a user namespace, as rootless Docker does. It copied that release’s CLI out of the running varlatchd container, which fails there because Docker cannot remount the container’s read-only secret files, and quietly used the gate of the CLI you ran instead. It now copies the CLI from varlatchd’s image.
Upgrading
Section titled “Upgrading”No database migration: 0.10.1 uses migration 20, like 0.10.0.
- From 0.10.0: download
varlatch-cli-0.10.1.cjsfrom thev0.10.1release, check it againstSHA256SUMS, and runnode varlatch-cli-0.10.1.cjs upgrade 0.10.1 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. Then replace the host CLI withvarlatch-cli-0.10.1.cjs. - From 0.8.0 or 0.9.0: follow the 0.10.0 instructions below with the 0.10.1 CLI and version.
- 0.10.1 restores everything 0.10.0 restores, and archives from 0.10.0.
0.10.0 (2026-09-25)
Section titled “0.10.0 (2026-09-25)”Before upgrading: validation changed. varlatch validate and the
dashboard now evaluate only the items the caller may read, and report the
rest as not evaluated. The CLI then prints INCOMPLETE and exits with a new
code, 2, where it used to report a verdict. CI jobs that validate with an
identity lacking secret.reveal or config.value.read need those actions
granted, or must treat exit code 2 as incomplete. See Security and
Upgrading below.
Project
Section titled “Project”- Varlatch is open source: AGPL-3.0-or-later for the secret store daemon, the
dashboard, and the dashboard backend functions, and Apache-2.0 for
everything else. See
LICENSE. - Vulnerabilities are reported privately, as
SECURITY.mddescribes.
Security
Section titled “Security”- Validation no longer reveals anything about values you cannot read. Before,
anyone who could see an environment could validate it and learn whether
each value, Secrets included, passed its contract checks. Now each check
needs the right to read what it describes:
secret.revealfor Secrets (including any Tailnet Requirement),config.value.readfor other values, andconfig.metadata.readto report missing items.secret.usealone is not enough. - Items you cannot read are listed as not evaluated, with the permission they
need, and an environment is reported as valid only when every item was
checked.
varlatch validateprintsINCOMPLETEand exits with a new code,2, in that case;1still means invalid. The dashboard shows valid, invalid, or incomplete. - Every decryption for validation is recorded in the audit log first
(
secret.validatedandvalue.validated, naming the item versions). The results themselves are never logged.
Self-hosting
Section titled “Self-hosting”varlatch setupcreates an installation interactively or from flags: generated secrets kept in files rather than environment variables, an escrow of the root key, and a choice of access. Public gets HTTPS certificates automatically, tailnet serves HTTPS on your Tailscale network only, and external sits behind your own proxy.varlatch adoptbrings an existing installation under the same management, step by step and reversibly.varlatch doctorchecks an installation’s health without changing anything.varlatch upgradecompletes only when the new release passes its health gate, and refreshes the access overlays the installation uses.- The dashboard reaches its live backend on the same origin (
/convex), so an installation needs one public address. The dashboard and the API keep working while the live backend is down, and fail fast instead of hanging. - The Coolify deployment is generated from the canonical Compose files, and each service receives only its own secrets.
Backups
Section titled “Backups”- Backups no longer pause the service: the secret store keeps serving while a consistent snapshot is taken.
- Archives hold the secret store only. The dashboard backend is rebuilt on restore instead of restored, which makes archives smaller and restores simpler.
- API clients, the CLI, and the dashboard wait out maintenance windows and say so, instead of failing.
Releases
Section titled “Releases”- Images for linux/amd64 and linux/arm64, each with a provenance attestation
and an SBOM. Releases from the public repository sign the images and
SHA256SUMS; verifying a release explains how to check. THIRD-PARTY-NOTICES.mdlists every third-party component with its license, in each image and release.- Smaller images: the daemon image no longer ships test tooling, and the deploy image installs only runtime dependencies.
pnpm dev:upruns the whole stack from a checkout, with synthetic data.
- The values editor commits against the versions you reviewed. Before, when another client changed a value while the review dialog was open, the dashboard’s live update made your commit overwrite that change without a conflict. Now it is rejected as changed since review, and your draft is kept.
Upgrading
Section titled “Upgrading”Releases now come from varlatch/varlatch on GitHub. Database schema:
migration 20.
- From 0.8.0 or 0.9.0: download
varlatch-cli-0.10.0.cjsfrom thev0.10.0release, check it againstSHA256SUMS, and runnode varlatch-cli-0.10.0.cjs upgrade 0.10.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive before applying migration 20, and completes only once the new release passes its health gate. That archive is still taken the old way, with the installation briefly paused, because online capture needs 0.10.0 to be running; plan a short maintenance window. - From the published 0.7.0: the offline path described under 0.8.0 still applies with this CLI.
- Then replace the host CLI with
varlatch-cli-0.10.0.cjs. - Installations set up by hand keep working as they are.
varlatch doctorlists them as not adopted, andvarlatch adoptmoves them to managed configuration one reversible step at a time. - 0.10.0 restores archives from 0.7.0 (migrations 16 and 18), 0.8.0 (migrations 18 and 19), 0.9.0 (migration 19), and builds made between 0.9.0 and 0.10.0 (migration 20).
CI jobs that run varlatch validate with an identity that lacks
secret.reveal or config.value.read on the environment now get exit code
2 (incomplete) instead of a verdict. Grant the identity those actions if it
should validate every item.
0.9.0 (2026-09-23)
Section titled “0.9.0 (2026-09-23)”Machine identities
Section titled “Machine identities”- Revoke individual service and OIDC credentials of a machine identity, including your own, and list credential metadata.
- Retire an identity, revoking all of its credentials in one transaction, and reactivate it later; its credentials stay revoked. Identities are never deleted.
- Rename identities; the audit log records the old and the new name.
- Last-used times for credentials and identities.
- CLI:
varlatch identity list|rename|retire|reactivate(retiring requires--confirm <name>) andvarlatch credential list|revoke. The dashboard’s Machines tab has the same actions.
Database schema: migration 19.
Backup operations
Section titled “Backup operations”- The operator CLI is a release asset,
varlatch-cli-0.9.0.cjs: one file, Node 22 or newer, no checkout or build. The varlatchd image carries the same file at/opt/varlatch/varlatch.cjs. - The reference host job verifies every archive right after capture, and a failed check fails the run.
- The remote-copy warning appears only when the latest remote check failed or none passed within 30 days.
- Reference systemd tooling (
infra/host-backup) takes its scripts and CLI from the running release, so upgrades need no host step.verify-remote.shverifies the remote copy periodically. - The SDK retries
503 MAINTENANCEresponses and honorsRetry-After. - PostgreSQL’s healthcheck probes over TCP, closing a first-boot race, and the dashboard and Tailscale containers gained healthchecks.
Upgrading
Section titled “Upgrading”- From 0.8.0: download
varlatch-cli-0.9.0.cjs, check it againstSHA256SUMS, and runnode varlatch-cli-0.9.0.cjs upgrade 0.9.0 --dir /YOUR/COMPOSE/DIRECTORY --bek-file /YOUR/BEK --kek-file /YOUR/ROOT-KEK. The upgrade captures and verifies an archive before applying migration 19. - From 0.7.0: the offline path described under 0.8.0 applies unchanged with this CLI.
- Then replace the host CLI with
varlatch-cli-0.9.0.cjs. With the reference host tooling, reinstallrun.shandverify-remote.shfrom this release once; later releases update them automatically. - 0.9.0 restores archives from 0.7.0 (migrations 16 and 18) and 0.8.0 (migration 18).
0.8.0 (2026-09-22)
Section titled “0.8.0 (2026-09-22)”Installation backup and recovery
Section titled “Installation backup and recovery”- Encrypted backups that only the operator can create, standalone verification, delivery to S3-compatible storage, and restore onto a fresh host.
- A restore stays isolated through daemon failures and host restarts, and an interrupted restore can be retried.
- The Installation settings page shows delivery and verification for each archive.
- Verification checks integrity, compatibility, and that the keys match. It does not load the dumps, so it is no substitute for a restore rehearsal against your own storage.
Upgrading
Section titled “Upgrading”- Keep the root key and a separate backup encryption key off the host. Recovery needs both keys, the archive, and a compatible release.
- Upgrade with the 0.8.0 CLI. From the published 0.7.0 (migration 16), the upgrade takes an offline capture: services stay stopped until it succeeds, so plan downtime. A source-built 0.7.0 at migration 18 takes an ordinary capture. Other versions are refused. The target schema is migration 18, on PostgreSQL 17.
- Both Compose variants need the shared
varlatch-statevolume and the Convex supervisor file. - Turn off automatic deployments that change the schema, for example in
Coolify. The old unencrypted
pg_dumpalljob is removed; existing dump volumes are kept. - To retry a failed upgrade, restore the original archive. Never run older binaries against a database that has been migrated forward.
See backups and recovery and the first-time B2 setup.
0.7.0 (2026-09-17)
Section titled “0.7.0 (2026-09-17)”- Sync Targets. varlatchd pushes an environment’s configuration to other platforms, so a rotation reaches every place that uses the value. A platform connection authenticates once per account; a sync target binds one environment to one destination as an explicit, audited disclosure, allowed only for someone who may read those values.
- Pushes converge on the destination’s state, deletions are opt-in, and a periodic repair pass fixes drift.
- Adapters for GitHub Actions (encrypted secrets), Coolify (host-pinned, with optional redeploy), and Convex. There is deliberately no generic push-to-URL.
- Sync is opt-in at every level.
VARLATCH_SYNC=offdisables it for an installation, andvarlatch sync pushpushes from the CLI where the server may not reach out. - Dashboard: integrations per environment with delivery state per value, and connections per organization.
0.6.0 (2026-09-16)
Section titled “0.6.0 (2026-09-16)”- Roles, groups, teams, requirements, and audit webhooks can be edited in place, protected against concurrent edits, with the old and new values in the audit log. Editing a role updates every grant that uses it.
- Changing a grant replaces it atomically, and authorization audit events record the facts each decision used.
- Search configuration item names across projects. Search matches names only, never values, and only in environments you may read.
0.5.0 (2026-09-16)
Section titled “0.5.0 (2026-09-16)”- Rotation without downtime. A rotated secret keeps its previous value available alongside the new one for a grace period.
- Custom roles, groups, and teams. A role is a named set of actions, a group a set of identities, and a team a group that owns projects. All of them resolve to plain grants, so access stays default-deny.
0.4.0 (2026-09-16)
Section titled “0.4.0 (2026-09-16)”- OIDC sign-in for CI, with GitHub Actions supported out of the box.
- Time limits and use budgets for service credentials.
- Signed audit webhooks.
- Value references (
${NAME}), expanded on the server. - Escrow for the root key: a passphrase-protected export, or k-of-n shares.
- An MCP server,
@varlatch/mcp-server, read-only by default. - The dashboard updates live from the audit stream.
0.3.1 (2026-09-09)
Section titled “0.3.1 (2026-09-09)”varlatch run --agent-metadata: the broker gives an agent a read-only credential, valid for at most an hour, to read configuration metadata, revoked when the run ends.
0.3.0 (2026-09-09)
Section titled “0.3.0 (2026-09-09)”- The credential broker.
varlatch run --agent-safestarts an agent with placeholders and no Varlatch credential. A local broker substitutes the real values only in requests to allowed destinations, and varlatchd authorizes and audits every use before decrypting anything. - The dashboard lists and revokes the capabilities agents hold.
0.2.0 (2026-09-09)
Section titled “0.2.0 (2026-09-09)”- The complete dashboard: changes applied as one set, explicit disclosure of secrets, a matrix of values per environment, access management for people and machines, the audit timeline, the contract editor, a command palette, passkey management, and a getting-started checklist.
- Grants scoped to a single environment.
0.1.0 (2026-09-08)
Section titled “0.1.0 (2026-09-08)”The first release:
- The encrypted secret store, with
kek verifyfor the root key. - Passkey-only sign-in, with bootstrap, recovery, and invitations as enrollment links.
- Grants and requirements, including retrieval restricted to your Tailscale network.
- Configuration contracts, managed in the dashboard or kept in the repository, with validation.
- The append-only audit log with NDJSON export.
- The
/v1API, the SDK, and the CLI, including sign-in through the browser. - The dashboard, with its live backend on Convex.
- Deployment with Docker Compose or Coolify, and digest-pinned release images.

