Changelog · Changelogs
locksql changelog
Release notes also list every commit on the GitHub releases page. This file records the changes a user or an agent has to know about.
Unreleased #
v0.18.2 (2026-11-14) #
Supported lockhub: v0.9.0 or later (unchanged). Built on lockkit v0.19.1 (unchanged).
Fixed #
- Hub mode: running (
locksql_runwithticket) or activating (locksql_grantwithticket) a ticket the human has not decided yet answereddenied: … hubclient: no decision yetwith "The human denied this; do not retry it". It now answerspendingwith the same ticket, like the first run. Only a denial answersdenied; a ticket that already ran answersno_such_plan, and a decision the console cannot verify anerrornaming the reason (#234).
v0.18.1 (2026-11-13) #
Supported lockhub: v0.9.0 or later (unchanged). Built on lockkit v0.19.1 (unchanged).
Fixed #
- Hub mode: a pending answer comes at once again. Since the lockkit v0.17.0 bump (#214), every pushed ticket waited up to 2 s for a decision locksql never used in that call (#230).
v0.18.0 (2026-11-13) #
Supported lockhub: v0.9.0 or later (unchanged). Built on lockkit v0.19.1 (v0.19.0 plus a perf-only patch of the PII finders).
Breaking #
- The launcher's
locksql consoleis gone: it mounted the fixedlocksql-runvolume, which no agent command mounts any more, so no client reached it. It now exits 2 withthe console is started by lox run only (lockhub): run lox run in the workspace.LOCKSQL_CONSOLE_NAME,LOCKSQL_READONLY_PROJECTand the launcher'sVAULT_*pass-through went with it (#155). - lockkit v0.19.0: the default PII detectors can mask more, or under
another kind (
fr_nirbeforecard, more phone formats, an e-mail with an apostrophe); secret-looking config values are now refused as "looks like it embeds a secret (a password or a token)"; the Claude Code notes oflocksql initintroduce the allowed calls neutrally (#209).
Behaviour #
- Hub mode: the console opens at most 30 tickets a human decides per 5
minutes (was 10) and 30 pending ones (was 20); a ticket
lox run's auto mode decides leaves that count, and a safety cap of 300 tickets per window counts every one (lockkit v0.15.0, lockkit#150). - The profile's
limits.tickets_per_window,limits.ticket_windowandlimits.max_pendingset those limits; more tickets, a shorter window or more pending tickets is a loosening, reviewed like the others (the typed profile name on production). They travel withlocksql config export. - A run or a grant over a limit answers the agent with the limit, the
count, when the next ticket is accepted and the key that raises it
(
not submitted: too many tickets: … retry after 42s), instead of a general "too many requests are waiting". limits.idle_timeoutcloses the database connection and the SSH tunnel instead of ending the console (#198). The console keeps running and its socket; the next request asks a connect ticket inlox run, and is served once approved, without the start questions (policy review, production name, unchanged privilege findings). A secret that isask(credentials,ssh.credentials) is typed inlox runand comes sealed to a one-use key of the console; the bastion's takes a second ticket.max_sessionstill ends the session.- The agent commands, from a sub-project with its own
.locksql/config.tomlbelow the workspace, stop (exit 2) and say why: lox run's console serves the workspace's.locksqlonly, so the sub-project's socket name matched no console (a generic "no console" before). A workspace path holding&,<or>is still refused, now saying so instead of "is not lox run's state" (#155). - An
UPDATEorDELETEwith noWHERE(orWHERE TRUE,WHERE 1,WHERE 1 = 1) and noLIMITgotverdict OK: it now getsverdict WARNand its screen sayswhole table: N rows(#208). pii add(andlocksql_pii_add) accepted a pattern that names no column and left the column in clear: on PostgreSQL,app.customers.namewithappthe database, where rules nameschema.table.column(public.customers.name). A pattern without*that matches no column of the catalog is now refused with the columns it may mean (did you mean public.customers.name?); a pattern with*that matches nothing yet is added with a warning.pii listflags the dead rules, which name no column (new console methodpii.unmatched). The tool descriptions and the CLI help sayschema.table.columnfor PostgreSQL (#207).
Security #
- A weaker
tlsmode (verify-fulltorequire, bykconfig.TLSRank), anothertls_caor anothertls_server_nameis now a loosening the human reviews: these changes were listed but applied without asking, so a profile could be pointed at a server it trusted less. A profile approved asverify-full(the former default) that now loads aspreferis asked once (#209). - Joins between two PII columns (
ON a.email = b.email,IN (SELECT ...),USING,NATURAL) passed as ordinary joins: the agent saw their row estimates andEXPLAIN, and the human got no warning, while a stored value the agent knows would tell it which subjects share it. They are still allowed and run no k-anonymity count, but the ticket and the audit record now warnjoins PII columns: <columns>(reads and writes), the estimates are hidden (estimates hidden: the statement joins PII columns), theirEXPLAINis refused (E_MASKED_FILTER) and no grant covers them (#150).
Added #
- The company's managed configuration (
managed.toml, lockfamily#31): locksql enforces its[locksql]section.[locksql.bounds]bounds every profile (tier, production, tls, credentials, engine, hosts, write_functions, credentials_ttl, auto rules, limits, required detectors and[[pii_mask]]rules always on); a profile beyond them is refused at start and at every policy poll with no prompt, oneblocked by company policy: locksql.bounds.<key> = <bound> (profile P: <field> = <value>)line each, auditedrefused-managed, exit 3.[locksql.defaults]fills the keys a profile leaves unset;[locksql.profiles.<name>]are served as written and cannot be redefined.[family] vault.requiredrefuses a profile whose credentials are notvault. The start screen showsManaged by <org> (sha256:…), approvals recordmanaged_sha256, audit recordsmanaged. Newlocksql managed showandlocksql managed check [--profile P](lox runs it before it starts the lock), and amanagedline inlocksql doctor. See docs/configuration.md (#209). - Launcher: it mounts the machine's managed directory (
/etc/lockfamily,/Library/Application Support/lockfamilyon macOS) read-only on/etc/lockfamilyin its containers, aslox rundoes in its locks:locksql managed show,managed check,doctor,profile,initand the other commands run from the host see the company's managed configuration. No directory: no mount; a file, a dangling link or a:in the path stops the launcher (docs/docker.md, #222).
Fixed #
- The trust question of an unknown SSH host key now carries its context
(the host and
<type> key fingerprint is <fp>) in the question file (lockkitPrintContext): a human answering fromlox attach, lea, the hub or the mobile app sees which key they trust, and the 8 characters a production profile asks for. The terminal shows the same lines (#213). - A ticket refused over the hub client's limits (
limits.tickets_per_window,limits.max_pending) could answer one ask with another ask's limit or retry-after: the last refusal was shared. Each ask now gets its own (not submitted: <limit, count, retry after>; <what to do next>), from lockkit v0.17.0 (#204). - Two locksql locks in one workspace left the agent with no locksql tool:
locksql mcpexited at start (several locksql locks: ...; pass --instance NAME). One server, aslocksql initwrites it, now serves every lock: each tool goes to the lock whose directory holds its profile's console socket, and the CLI commands do the same with--profile. The launcher mounts everylox runlocksql lock of the workspace, each on/run/locksql/<instance>, as lea does.mcpnever exits at start over the instance:locksql_statusreports the problem (#206). - Writes while mask rules exist (#208): an
INSERTwithout a column list (INSERT INTO orders VALUES (...)) was refused as a masked write (E_MASKED_WRITE, retry stop) even into a table with no masked column. Its columns are now resolved from the catalogue, in the table's order, and it is refused only when a value lands in a masked column. - Hub tickets said personal data was touched (
personal_column) for every statement with a literal. They now say so only when the statement names a column under a mask rule, and the screen names those columns; literals stay masked (#208). - A statement with a data-modifying CTE passed
query.planand was refused only byquery.run: the plan refuses it now, with the sameE_STATEMENT_KIND(#208). - With
--instance,statusand the tools no longer report the profiles of another mounted lock as down, nor ask for aprofilethe lock does not need (#206). - The launcher's
statsandforgetmounted the volumelocksql-home, which no console uses sincelox runstarts them:statsread an empty or stale audit log,forgetremoved a secret no console reads. They now mount the home volume of the workspace's locksql lock thatlox runruns (lockfamily-<hash>_<instance>-home), found and checked as the agent commands find its socket,--instance NAMEfor one of several. Without lox run's state, they still mountlocksql-homewhen it exists, and never create it (#220).
v0.17.2 (2026-11-07) #
Supported lockhub: v0.9.0 or later (hub-mode tickets are now version 2, which older hubs refuse). Built on lockkit v0.14.1.
Fixed #
- Hub mode: every plan ticket names its kind (
read,writeorddl; admin statements count asddl) and its profile, and grant tickets arereadtickets of risk grant (#195, #197). They were version 1, solox run's auto mode asked every one, even a maskedSELECTunderread = auto. Risk, never-auto and production flags are unchanged. tls_ca,tls_certandtls_keyexpand a leading~/to the console account's home, asssh.keydoes; the console checks every file key right after the policy review, before any secret is asked, andlocksql doctorreports a missing one with the command that copies it into the console's home (#194, #196).
v0.17.1 (2026-11-07) #
Supported lockhub: as v0.16.0. Built on lockkit v0.14.1.
Changed #
- Every user- and agent-facing text names
lox run, which starts the hub and the locks and serves the decisions in a terminal of its own, outside the agent, instead oflox up, deprecated since lockhub v0.10.0 (#192): the docs and the site, the agent assetslocksql initwrites (skill, AGENTS.md, config.toml), the MCP server's instructions, the launcher's errors,doctor,initand the console's messages. The shared texts come from lockkit v0.14.1. No behavior changes: container labels,.lockfamily/up.jsonand the flags stay the same.
v0.17.0 (2026-11-06) #
Supported lockhub: as v0.16.0 (tickets stay version 1). Built on lockkit v0.13.0.
New #
- Auto mode per profile (#185, #186).
auto = "full-auto"or an[auto]table per profile in.locksql/config.tomlsetsconnect,read,write,ddlandunmasktoauto,askordenyforlox run's auto mode (defaults: connect ask, read auto, write/ddl/unmask ask).denyis refused by the console (E_AUTO_DENY),asktickets are never-auto, and loosening a rule is a reviewed policy loosening. Newlocksql profile rules [--json]; the audit recordsauto_ruleanddecided_by.
Changed #
- With the default rules, write, DDL and unmasked-read tickets are now never-auto: lockhub's renewal re-approval and any auto mode no longer approve them without a human (#186).
- The generated skill and
AGENTS.mdtell the agent to end its turn on a pending ticket or grant and to run it when told it is approved, instead of asking the user to report the approval (#176, #187).
Security #
github.com/klauspost/compressbumped to v1.20.2 (GO-2026-6865) (#187).
v0.16.2 (2026-11-05) #
Supported lockhub: as v0.16.0. Built on lockkit v0.12.1.
Changed #
- The console keeps the question it waits for (the prompt, never the
answer) next to its socket, in
<project>-<profile>.question, removed once answered:lox attachand lea show the question actually pending after a restart, instead of guessing "its profile name" while the console waits for the Vault token (#182).
v0.16.1 (2026-11-04) #
Supported lockhub: as v0.16.0. Built on lockkit v0.12.0.
Changed #
- A production console asks for its profile name once per approved
policy, not at every start (#178). The name is typed when the policy is
first approved and after a reviewed loosening; a restart of an approved,
unchanged production profile (or one only tightened since) shows the
banner and goes on to the credentials (a Vault token, when the profile
needs one, is still asked). The console records the policy the name was
typed for beside the approved policy (
approved/confirmed-<key>.json); without that record, as on the first start after the upgrade or after a wrong name, the name is asked.
v0.16.0 (2026-11-03) #
Supported lockhub: v0.3.0 or later; grants need lockhub v0.6.0 or later (risk
class grant): an older hub refuses the grant's ticket, everything else works
as before. Built on lockkit v0.12.0. No :review on upgrade: an unchanged
policy keeps its fingerprint.
New #
- Grants: one approval for a bounded read scope, against approval
fatigue (#171).
locksql_grant(CLIlocksql grant TABLE...) asks for a database, tables, a duration and a budget (statements run, rows and bytes returned); the console resolves the tables, clamps the request to the profile's limits and submits a ticket of riskgrant, alwaysnever_auto, whose plan hash is the hash of the scope's canonical JSON. The approval screen readsgrant on <instance>with one line each for the tables, the duration and the budget as clamped; production asks for the instance name. Once decided, the agent activates it (locksql_grantwithticket,locksql grant TICKET) within the ticket's 4 h; the duration counts from then. While it lasts,locksql_runruns the agent's masked reads whose every table (CTEs and subqueries included) is in the grant without a ticket: one statement each, capped at the rows left and, for each of its JSON rows and TSV text (which carries exactly the JSON's rows), at half the bytes left; both renderings are charged after the run, so usage never passes the budget, and the read runs only if the rows left cover the EXPLAIN estimate. Writes, DDL,unmask, any read that filters, groups or aggregates on a PII column, any read whose approval screen would carry a warning, and any read that uses a column no allow rule names other than as a plain select-list column (a filter, join, grouping, ordering,DISTINCT, a compared subquery or any expression) always get a ticket when that column is of a text, JSON, binary or unknown type (on SQLite, any column of a table that is notSTRICT), or a number while a detector that matches digits is on (every detector butemail; a single-column primary key the database generates, such as an auto_increment, serial or identity id or the SQLite rowid, excepted): such a predicate would probe the values the detectors mask. A text or number column in a filter needspii.allowto stay covered. A grant needs the agent's lea token (LEA_AGENT_TOKEN), to ask, activate and run; only that agent uses it, and the first covering grant is the one charged. - New limits
grant_max_duration(default"1h","0"turns grants off),grant_max_statements(200),grant_max_rows(10000) andgrant_max_bytes(1 MiB). Raising one, or turning grants on, is a loosening. An unchanged policy keeps its fingerprint: no:reviewon upgrade. :revokeends every grant,:revoke IDone. Grants also end with their time, the session, or any change of the approved policy (a tightening too, auditedpolicy_changed; added mask rules alone end none, and a plan made before a policy change is never covered), and never survive a restart.:statusandlocksql statuslist them with the time and budget left.- Audit:
grantandgrant_endrecords, and the records of every covered run, carry the grant id (grant,ticket) and the hub's signature of its decision (hub_sig);grant_scopedescribes the scope, andgrant_endrecords what the grant used (statements_used,rows,bytes_used). - New refusal codes
E_GRANTS_OFFandE_GRANT_NEEDS_AGENT(bothask_user).
v0.15.0 (2026-11-02) #
Supported lockhub: as v0.14.0 (v0.3.0 or later). A minor release: a remote
profile with no tls setting now connects with prefer instead of
verify-full (see Upgrading).
Upgrading #
- Remote profiles without
tlsmove fromverify-fulltoprefer(#169). On the first start after the upgrade, a remote profile approved underverify-fullliststls: verify-full → preferonce and applies it without asking. To keep verifying the server's certificate, settls = "verify-full"in the profile.
Breaking change #
- locksql no longer enforces transport security; the driver does (#169).
The
tls,tls_ca,tls_cert,tls_keyandtls_server_namesettings are passed to the PostgreSQL and MariaDB/MySQL drivers with libpq'ssslmodesemantics, and locksql checks only that they are well formed.- A profile with no
tlssetting getsprefer, the drivers' default, whatever the host (it wasverify-fullfor a remote host since v0.4): a server with a private or self-signed certificate connects with no extra setting. Settls = "verify-full"to keep verifying it. production = trueloads whatever itstls: a production server that offers no TLS needs no workaround (#170).- No authentication method is refused because of the TLS mode any more
(PostgreSQL
password/md5, MySQLmysql_clear_password,sha256_password, acaching_sha2_passwordfull authentication); only the driver or the server can refuse a connection. The MySQL engine uses the driver's own TLS instead of its STARTTLS layer. tls_ca,tls_certandtls_server_nameare no longer refused under a mode that does not use them, norrequireand stronger on a Unix socket.
- A profile with no
Changed #
- One plaintext warning, nothing else. When the established connection
is plaintext, the console prints
warning: the connection to the database is NOT encrypted (tls = ...)at session start and audits it as a notice; the session proceeds. A TLS connection gets no warning, whatever its mode: the "certificate is not verified" notice is gone. - A change of a
tls*setting is no longer a loosening. On the first start after the upgrade, a remote profile withouttlsliststls: verify-full → preferonce and applies it without asking. locksql doctorshows thetlsmode as information and never warns or fails on it; it no longer checks the TLS files nor probes the server's certificate.profile addproposesprefer.
v0.14.0 (2026-11-01) #
Added #
--instance NAME: reach one lock among several of this type (lea mounts each at /run/locksql/NAME). Every client command,status,mcpanddoctortake it; it defaults toLOCKSQL_INSTANCE, then to the one lock listed inLEA_LOCKSQL_INSTANCES.
v0.13.1 (2026-11-01) #
Supported lockhub: as v0.13.0 (v0.3.0 or later).
Fixed #
doctor --fixno longer breakslox up(lockhub#92). It gave the project's.locksql/to the console account (uid 10001), a leftover of the consoles started by the launcher, which read.locksql/themselves; the consoles run underlox uponly now, which copies.locksql/as you and refused it afterwards (owned by uid 10001, not by you).doctor --fixnow keeps.locksql/aslox upreads it: directories and regular files that are yours and that no one else can write. It removes the write bits of group and others there (chmod go-w), and gives back to you, as root, what the console account owns in it: the launcher runs that one repair in a root container of its own, only when.locksql/holds an entry that is not yours, its output on stderr. It changes each entry through a handle opened without following a link, and never a hard link. Runlocksql doctor --fix --yesonce on a project an olderdoctor --fixrepaired.doctor --fixpasses on a fresh setup. The launcher ran every check as root, soclient accessalways failed (root is not inlocksql-clients) and the run exited 1. It now runs as you (--useryour uid and gid, your home mounted for the agents' wiring, the grouplocksql-clientsfor the socket directory), and a uid with no account in the image passesclient access: the launcher runs the agent's commands asagent. Plaindoctorstill runs asagent; both check.locksql/for you (LOCKSQL_DOCTOR_FOR, your uid and gid, set by the launcher).- Removed: the project checks of the console that read the project.
project access(with itssetfaclrepair for the console account),project .locksql/pii,project config.toml/pii.toml/pii/<set>.toml(readable by the console account) andPII rule sets: the console readslox up's reviewed copy, never the project.
Added #
- Each release tag bumps the
locksqlformula of the Homebrew tap (brew install davidgodefroid/tap/locksql): the release workflow's last job dispatches the bump once the release is signed (DavidGodefroid/lockfamily#8). It is skipped while theTAP_DISPATCH_TOKENsecret is not set.
v0.13.0 (2026-11-01) #
Supported lockhub: as v0.12.0 (v0.3.0 or later). A minor release: a script
that runs profile add without a terminal needs --non-interactive (see
Upgrading).
Upgrading #
profile addwithout a terminal needs--non-interactive(lockkit#111, the launcher verbslox initcalls). A script that passed--engineand its flags without a terminal now gets a usage error: add--non-interactive. There--credentials keychainis refused (askorvaultonly); the wizard still offers the keychain outside a container. A profile already in the config exits 3 as before, and a flag's errors name it without quoting the value.doctor --fixasks before each repair on a terminal, the ones of this account (wiring the agents) as well as the root ones, unless--yes; without a terminal--fixneeds--yesand is refused before any check runs (it used to run the checks and make this account's repairs first).
Added #
- The launcher verbs
lox initdrives (lockkit GUIDE §14, lockkit#111):locksql init <agent>,locksql profile add,locksql doctor, with the shapes every lock shares (docs/usage.md).profile add --non-interactivenever reads the terminal and takes everything from the flags; a missing--engineor--pathis a usage error naming the flag. It checks its arguments with lockkit'sverbsbefore writing anything: a flag named like a secret (--password), a value that embeds a password, a positional argument and any credential source other thanaskorvaultare refused.profile add --productionwrites a production profile; the wizard asks for it too. The wizard asks only for what the flags did not give.profile add --jsonanddoctor --jsonprint exactly one line of JSON on stdout ({"lock","profile","config","production","credentials"},{"lock","ok","fixed","checks"}, or{"lock","code","error"}withcodeusage,secret,existsorfailedon failure); the human text goes to stderr.
Changed #
- lockkit v0.10.0 (was v0.9.1): the launcher verbs contract (
verbs).
v0.12.0 (2026-11-01) #
Supported lockhub: v0.3.0 or later (the host CLI lox, and the
.lockfamily/up.json the agent commands read); the console now needs it
(docs/usage.md, lockkit#110). A minor
release: it breaks standalone use (see Upgrading).
Upgrading #
- The console runs attached to lockhub only.
locksql consolewithout--hub,--hub-key-fileand--instanceexits withlaunched by `lox up` only(exit code 3), before any prompt or file write: start it withlox add locksql <instance>andlox up, and decide the requests withlox approve. A script or service that started a standalone console stops working. - Plans a console started with
--hubleft pending are no longer withdrawn by a later start without it: there is no such start. - The agent commands reach the console
lox upruns (lockhub#78). The launcher'smcp,status,tables,describe,plan,run,request,logout,piianddoctormount the socket volume of the workspace's locksql lock thatlox upnames in.lockfamily/up.json, read-only, once Docker confirms it is lox up's (the checks of lea; no container lox up did not start may mount it read-write), and no longer the fixedlocksql-run: withoutlox upthey exit 2 and say to run it (doctorruns without a socket and says why). With several locksql locks in the workspace,--instance NAMEpicks one (in.mcp.json:"args": ["mcp", "--instance", "NAME"])..mcp.jsonis unchanged (docs/launcher.md).
Changed #
- lockkit v0.9.1 (was v0.9.0). When no console runs, the hint is
lox up: the CLI (exit code 2), everylocksql_*tool andstatusanswerthe console is started by `lox up` only: ask the user to run `lox up`, and thatlocksql doctorchecks the setup, andstart(--json, thelocksql_statusstructured content) islox up, no longerlocksql console --profile P --project DIR, which refuses to start without its hub flags. The hint names no profile or project (lockkit#114).doctor's console check, the MCP server instructions and error hints, the agent skilllocksql initwrites,init's next steps, the docs and the site point atlox up, and atlox attach <instance>for the console's start-up questions (policy, PII rules, password). doctor's PII rule sets check sayslox up(a lox up workspace), no longerlock up; so do the docs and comments (lockhub#79). The hub end-to-end test builds lockhub'scmd/loxonly.
Removed #
- Standalone mode: the per-request approval prompt on the console's
terminal (
Approve? [y/N], the production profile name,n <reason>,Press Enter to refuse), its timeout and its typed approvals. Everyquery.runof a plan answers pending with a ticket decided in lockhub. The console's own decisions are still asked on its terminal: a loosened policy (at start and at:review) and:unmask, with the approver of its security level (typed, or the fingerprint at level 2). - Auto mode:
--auto, its older name--skip-permissions, Shift+Tab and:modeno longer switch anything (lockhub decides every statement), and the:statusmode row is replaced by the lockhub instance that decides.locksql statusno longer printsauto-approve; theauto_approvefield of the JSON status stays, always false. - Typed values and cell references: placeholders (
'${name}') and cell references ('${rN.R.C}') are not supported (no value is typed in lockhub).query.plannow refuses a statement holding one, read or write, withE_PLACEHOLDERand a reason saying so, before it is analysed (it used to be planned and refused only at run). Masked cells come back as plain<redacted>, never<redacted:rN.R.C>: the console no longer keeps the clear values of masked cells in memory, nor resolves references while it plans. The--show-resultstable no longer marks cells with‹rN.R.C›, the plan answer has novaluesfield, and the reference probing warnings are gone (limits.reference_probeis still read, without effect). The "agent received this value in clear" warning no longer suggests a placeholder. The MCP instructions, thelocksql_plansqlschema, theE_PLACEHOLDER,E_FORBIDDEN_TOKENandE_MASKED_FILTERhints, the agent instructions written bylocksql init, the docs and the site no longer teach placeholders, references, terminal approval or auto mode.
Fixed #
- The run of a ticket the weight check refused under a PII filter is
audited (
refused, with the weight reason andE_COST), as the terminal refusal it replaces was; the client still gets the code and its default hint only. - The
approvedandrunrecords of a ticket carry the k-anonymity warnings of its counts, as itspendingrecord does. - A hub denial reads
denied by the human: <reason>; do not retry unless asked, no longerdenied by the human: denied: <reason>; ...; the reason is taken from the hub's decision. - The
E_MASKED_WRITErefusal (a masked column may only be set toNULLorDEFAULT, unchanged) gives its current reason: a planted value would come back masked yet known to the agent, and a join of that column with=to another PII column, which no k-anonymity count covers, would tell it whose value matches. It no longer speaks of cell references. - An approved ticket whose statement cannot be planned again before its
run (the catalog cannot be read, the connection fails) is audited: the
hub's signed
approvedrecord, with the class, verdict and k-anonymity warnings of its ticket, then arunrecordfailedwith the redacted error; the run answered the error but left no record before. - A plan that fails in the engine (catalog or
EXPLAINread, object lookup) answers the agentplan failed: statement refused by the database (the details are shown on the console), no longerplan: <the database's text>, which could quote a value; the console shows the redacted detail. - The other requests the database fails no longer hand its text to the
agent either:
catalog.listandcatalog.describe, and the checks before a ticket is submitted or run (the object state of a guarded plan, the function lookup of the k-anonymity counts) answer<operation> failed: the database returned an error (the details are shown on the console)instead of<operation>: <the database's text>, which could quote a value. The console shows the detail, redacted when the session masks; a failed catalog request is audited with it redacted too (it was audited unredacted). A lost connection still answersconn_lost, and a check cut by the statement timeout answers<operation> timed out (limits.statement_timeout). - A privilege audit the database fails at start shows in
status(and so inlocksql_statusanddoctor) asprivilege audit failed (the details are shown on the console), no longer with the database's text; the console shows the detail.
v0.11.5 (2026-10-31) #
Supported lockhub: as v0.11.4 (lockhub serves /v1/ at its root, so the
http(s)://host[:port] hub URL lockkit v0.8.1 requires is the one it
always needed).
Changed #
- lockkit v0.8.1 (was v0.7.2). The
--hubURL ishttp(s)://host[:port]only: a path (https://h/lockhub, which never reached a working hub: lockhub serves/v1/at its root and signs the path), a query, a fragment, an IPv6 zone, a host that is not a DNS name or an IP address, or a port outside 1-65535 is now refused, without quoting the URL. The console's start line and the separated-mode handoff show and pass the hub in that canonical form, and a hub that cannot be reached is named by scheme and host, no longer by the whole request URL (lockkit #107). - lockkit v0.8.0: the PII detectors read the
\n,\rand\tescapes and the\u00XXescapes of&,<,>,=and quotes (JSON log lines) as the blank or delimiter they stand for, so a value right after one is masked ({"msg":"tel:\t0475123456"}). The new opt-in detectors (ip,secret,person_name) are not enabled: the default detectors mask what they masked before, apart from this. - lockkit v0.8.0: the Claude Code wiring, merging into the user's
~/.claude/settings.json, writes thepermissions.allowentries already there with&,<and>as written instead of\u0026-style escapes. locksql's own entries and note hold none of these characters: what it adds is unchanged. console --profilein a separated setup is checked before the handoff: started from another account than the console's, the console command is printed or run through sudo, machinectl or su only for a profile of the project's.locksql/config.toml; another--profileis refused (unknown profile: --profile names none of the project's profiles (...)) instead of being put on a command line every account can read. A profile kept only in the console account's own user config is started from that account.
Fixed #
- A
--profilenaming no profile is not quoted back (it may be a token or a URL pasted in the wrong place): the client commands (tables,describe,plan,run,pii ...),status,mcp,forget,console,doctor,config export --profileandconfig import --profile/--localname the flag (and the configured profiles), never the value given. An unknown command is not quoted back either. With lockkit v0.8.1, the same holds for an invalid profile name,--instance,--require-device,hub-key's flags, the console's--profilemismatch and itsservice_user/client_grouprefusals.
v0.11.4 (2026-10-31) #
Supported lockhub: as v0.11.3.
Fixed #
- Config errors never quote a value. A secret pasted in the wrong key
(
credentials = "<token>") no longer ends up in the terminal, the logs or the agent's output: the errors of the config file (tier,engine,tls,credentials,write_functions,detectors, thelimitsdurations), ofconfig import(needs_local, the PII rule set names), of the PII rules files (a rule'scolumnandmode), oflocksql profile add(profile name, engine, credentials, vault path, tls), ofconfig import --local,console --allowed-uids,console --instanceandLOCKSQL_PII_DIRname the key and the allowed values, never the value given (#143). A PII rules file error names the entry ([[mask]] entry 2: pii rule: unknown mode (want redact, partial or email)). - lockkit v0.7.2 (was v0.7.1): the same for
ssh.credentialsand the--hubURL, which the hub client's errors now show by scheme and host only; a--hubURL with a user or password is refused (#143).
v0.11.3 (2026-10-31) #
Supported lockhub: as v0.11.2.
Changed #
- The SQLite engine embeds SQLite 3.54.1 (
modernc.org/sqlitev1.61.0,modernc.org/libcv1.79.0; was 3.53.4, #107). A statement cancelled, or past its timeout, while it waits for a lock underbusy_timeoutnow stops then instead of waiting out the 2 s, and one whose context is done before it starts stops within 1000 VM steps instead of running to completion; a write's COMMIT blocked by a reader (rollback journal) still waits the 2 s. - Release signing is unchanged: CI and the release workflow use
sigstore/cosign-installerv4.1.2, still pinned to cosign v2.6.1 (#108).
v0.11.2 (2026-10-31) #
Supported lockhub: as v0.11.1; allowed_uids on a locksql lock under
lock up needs lockhub v0.2.5 or later (#139).
Added #
locksql console --allowed-uids UID,...: these uids may connect besides the members oflocksql-clients, asallowed_uidsof the image'ssystem.toml(theALLOWED_UIDSbuild argument) does, for that console only, with the release image unchanged: an agent in a container of its own whose uid has no account in the console's image (lea's agents run as the host user's uid, #139). Set only by whoever starts the console (the human: the launcher and the handoff to the console account pass it on;lock upof lockhub v0.2.5 a lock's reviewedallowed_uids); root, leading zeros, 65534 (nobody) and the console's own uid are refused; every isolation check sees the uids (a session or terminal of one is the agent's), and the console prints them at start.
Changed #
- The
ALLOWED_UIDSbuild argument refuses root (0), leading zeros, the console account (10001) and 65534 (nobody); it took any digits before.
v0.11.1 (2026-10-30) #
Supported lockhub: v0.2.1 or later; v0.2.2 or later under lock up
(docs/usage.md, #135).
Upgrading #
- Records written by consoles before v0.11.1 stay unsigned under
lock audit verify --lock; their hub denials and the approvals that expired before their run show there as decisions not in the log, not as problems.
Fixed #
- Hub decisions are audited with the hub's signature (#118, #133).
The
approvedrecord of a ticket decided in lockhub now carrieshub_sig(therunrecord after it does not), so that lockhub'slock audit verify --lock INSTANCE=PATHchecks it against the decision the hub signed instead of reporting it unsigned. The hub's denials are now audited too (denied, with the reason, approver, facade andhub_sig), and so is the agent-side renewal (the run of an expired ticket, not audited before; renewals asked in lockhub already were): arenewrecord for every answer, the new ticket (also when planned again as stale) or refused with why (#136, #137). That record carries the approver, facade andhub_sigwhen an approval came with the expiry (an approval the console could not use; only the first run of the ticket). locksql doctorin alock upworkspace (#130, #134). In a project whoselockfamily.yamllock up reads (.lockfamily/, or the root when there is no.lockfamily), a.locksql/or.locksql/pii/the console account does not own (but can read) now warns instead of failing with achownrepair: lock up's console keeps its PII rule sets in its own volume (LOCKSQL_PII_DIR). Unreadable by the console account, writable by group or others (now with achmod go-wrepair), or a symbolic link still fails; outside a lock up workspace nothing changes.
v0.11.0 (2026-10-30) #
Added #
LOCKSQL_PII_DIR(lockhub#41). With it, the console keeps its PII rule sets in that directory instead of the profile's:lock upsets it to the console's own volume, its copy of the profile being read-only, so a first start no longer needs a rule set written beforehand by locksql's own launcher. The profile's sets are copied in once; from then on the console's copy is in force. At each start the profile's new mask rules are added to it; nothing else of the profile's set is (allow rules, removed masks, mode changes), and a profile set that still differs is told at start. It must be an absolute path (a relative one is a config error).
Changed #
-
Hub mode: re-approval, reveal and rollback (lockkit v0.5.0 and v0.6.0).
- An expired ticket (pending past its TTL, or approved but the approval
aged before the run) is asked again:
query.runwith it answerspendingwith a new ticket that names the expired one and the inputs of the plan hash that moved since (state: the definitions the statement resolves to,policy_fp,sql, …; names only, never values). The statement is planned again first, read-only, as the run's recheck does. A renewal asked for in lockhub is checked too: the policy and the profile now; the statement and its definitions are nameduncheckedon it (no session there), and checked at the run (stale, with a new ticket, if they moved). A changed policy is no renewal: plan again. Before, an expired ticket answereddenied. - The clear approval screen stays in the plan, for a pinned local facade
to reveal (Ctrl-O,
--facade-keys-file) or a pinned device; the hub still gets the masked one. Reveals and renewals are audited (reveal,renewrecords). - The separated-setup handoff forwards every hub flag lockkit registers
(
--facade-keys-file,--devices-file,--require-device,--seal-clear,--hub-plain-http); a relative facade keys or devices file is made absolute like the hub key file. --facade-keys-fileand--devices-fileare trust anchors checked like--hub-key-file: a link, a file or directory writable by its group or others, or one owned by neither root nor the console's account is refused before any prompt, and the console reads its own 0600 copy in its state directory.- Hub mode off (rollback): a console started without
--hubafter running with it withdraws the plans hub mode left pending (the hub hears of it at the next start with--hub) and answers a run of their tickets with the errorwithdrawn(CodeWithdrawn), instead ofinvalid_params. Nothing of them runs.
- An expired ticket (pending past its TTL, or approved but the approval
aged before the run) is asked again:
-
The console runs on lockkit's console (#96). The request loop, the live switches (Shift+Tab, Ctrl-O,
:unmask),:status,:reviewand the policy polling, the session limits and the reconnection are lockkit'sconsole.Server; locksql is its backend (the SQL methods, the approval screen, the masking). The approval screens, thestatusanswer,:statusand:revieware unchanged and pinned by the golden screens. Other differences:- a tightening read from the files that cannot be saved as the approved
policy now applies anyway (audited
policy/unsaved); before, the looser policy stayed in force.pii.addthen answers ok; - a tightening locksql cannot compile (an unknown detector) ends the session instead of leaving the looser policy in force;
- a change request beyond the 20 queued is refused ("too many pending change requests") instead of dropping the oldest; an empty or too long one says "change must be 1 to 500 bytes";
- any request naming a hub
ticketon a console without its hub is answeredCodeWithdrawn, whatever the method; - a request handler that panics is told on the console (escaped) and
audited as a failed
run; - at level 2 the fingerprint prompt names what it decides ("to apply the policy changes");
- credentials that expire without a way to reconnect end the session as "connection lost" (was "credentials expired");
- a change request is no longer checked for valid UTF-8 (it is escaped for the console either way);
pii.add: a rule the on-disk rule file refuses answersCodeInternal(wasCodeInvalidParams), and error texts go through the sanitiser;- in hub mode, three failed writes in a row of the hub's own records
(reveals, renewals) end the session, counted apart from the console's
own records; while a loosening approved at
:reviewis being saved, a renewal asked from lockhub may see its fingerprint, and goes stale at the run if the save fails.
- a tightening read from the files that cannot be saved as the approved
policy now applies anyway (audited
-
Pinned to lockkit v0.7.1.
-
Plan hash coverage (#119). The plan hash also covers the operators a view, rule, policy, default or
CHECK(of a table or a domain) uses, with their functions and those of their commutators and negators (pg_dependrecords them against the operator, not its function), the enum labels in their sort order and the attributes of the composite types of the columns, and, for aDROPorALTERwithCASCADE, every relationpg_dependsays it would drop or change along with its relations (views over them, policies, constraints, defaults and triggers that use them, inheritance children, owned sequences and their users, columns of their row type, recursively; the functions and domains it would drop are not hashed, andDROP FUNCTION/DOMAIN/SCHEMA ... CASCADEorDROP TYPEof an enum are not walked). The function of an operator counts as one more level of the 8-level bound. Plan hash version 3: plans made by v0.10.0 are answeredE_STALE. -
Hub-mode rowcount bound follow-ups (#120). On MySQL/MariaDB and SQLite the ticket of a write also names the triggers of the tables it names. An
INSERT/REPLACE ... VALUESis bounded by at least its number of rows (EXPLAIN gives no estimate for them on MySQL and SQLite; twice that forREPLACEandON DUPLICATE KEY UPDATEon MySQL/MariaDB). A write whose rows cannot be checked before a commit (a non-transactional table on MySQL/MariaDB) is refused withE_STATEMENT_KINDinstead of the generic failure, so the agent does not retry it. The MySQL/MariaDB check reads only the tables of the current database and of the schemas the statement qualifies a name with: a non-transactional table of another schema no longer refuses an unrelated write that uses the same word, and the server's own schemas are no longer exempt when named bare from within them.
v0.10.0 (2026-10-30) #
Changed #
-
Approvals are decided by lockkit's console. The console closes its approval screen with lockkit's
console.Asker(same prompt, same answers, same audit records): the screens are pinned byte for byte by golden tests (internal/console/testdata/screens). The upcoming hub mode then plugs into one place shared by every Lock. -
Chained audit log. Each audit record now ends with
prev_hash, the hash of the line before it (lockkit'saudit.Verifychecks the chain and names the first record edited, removed, inserted or reordered). Apart fromprev_hasha record is the 0.9 line, byte for byte. Existing logs are not rewritten: new records chain onto their last line. Records of hub mode will also carryticket,approver,facadeandhub_sig.locksql statsreads old and new logs alike. -
Plan hash.
query.plananswers aplan_hash(sha256:…) of the normalised statement, the profile, the policy fingerprint and, on PostgreSQL, the effectivesearch_pathand the definitions of every object the statement resolves to: functions and operators (body, settings,SECURITY DEFINER, grants), views (pg_get_viewdef), rules, triggers and their functions, row-level policies and the row-level security switches, grants, column defaults and generated columns, constraints, partitions and inheritance, followed through views, rules, triggers, policies, defaults and constraints (and, for a write, into the tables whose foreign keys cascade from it). Objects nested deeper than 8 levels refuse the plan rather than go unhashed. Not followed: the functions a PL/pgSQL body calls (only its own source is hashed) and what dynamic SQL reaches.query.runchecks it before the approval, then again in the statement's own transaction just before and just after the statement: a plan whose objects changed is refused with the new codeE_STALE(the statement rolled back when they changed during it).CREATE OR REPLACEkeeps the OIDs, so the definitions are hashed, not the OIDs. The hash also covers the domains of the columns (base type,NOT NULL, default,CHECKconstraints and their functions, through a domain over a domain) and, for a DDL statement, the event triggers and their functions (#105). A write follows only the tables whose foreign keys reference it with an action that changes their rows (CASCADE,SET NULL,SET DEFAULT): aNO ACTION/RESTRICTkey only refuses the write (the key itself is hashed), so a change in a wide graph of such tables no longer answersE_STALE. Plan hash version 2. -
Hub mode (
locksql console --hub URL --hub-key-file PATH --instance NAME, written bylock up;locksql hub-key --instance NAMEprints the instance key to pin). The human decides in lockhub instead of this terminal:query.runanswers{"status":"pending","ticket":…,"instance":…}(a result, not an error; over MCP, lockkit's standard pending answer), the plan kept in lockkit's persistent plans store, bound to the client's account, the policy fingerprint and the plan hash; the ticket carries the approval screen with every value of the statement masked.locksql_runwithticket(CLI:locksql run TICKET) runs it once the hub's signed decision approves it: the statement is planned again from its stored text and its plan hash compared before anything runs, then checked again in the statement's transaction. A changed plan answers{"status":"stale","ticket":<new>}with a new ticket; a denial carries the human's reason.--autois refused with--hub, Shift+Tab does not turn auto mode on, and statements with${placeholders}are refused (values are typed in the console). -
Plans and tickets bound to the requesting agent (hub mode, #91).
query.planandquery.runcarryrequester(the agent's lea name and the hash of itsLEA_AGENT_TOKEN, from lockkit'smcpkit.RequesterFromEnv; the CLI and the MCP server send it). A plan id (in every mode) or ticket presented by another agent is refused withCodeWrongRequester(-32007) and stays its requester's. Not a security boundary: agents of one account can read each other's token. -
Rowcount re-check of a hub-mode write (#91). A write's ticket shows
rows changed: at most N(twice the planner's estimate from anEXPLAINof the write, at least 10 more); the run counts the rows it changed in its own transaction before the commit, and past the bound rolls back and answers stale with a new ticket whose screen says how many rows it would have changed. PostgreSQL, MySQL/MariaDB and SQLite. The bound counts the statement's own rows: the ticket names the triggers, rules and cascading foreign keys it does not count (PostgreSQL); a data-modifyingWITHis refused in hub mode; on MySQL/MariaDB a write to a non-transactional table or a view is not run, and aROLLBACKthat warns is a failure. The screen names the requester by its kernel identity, the lea name as its unverified claim. -
Pinned to the lockkit v0.3.1 release (hub support) instead of a commit of lockkit main.
Fixed #
-
Becoming production at runtime re-applies the start-up refusals (#93, as lockkit#34). When the policy in force becomes production while the console runs (a tightening, applied at once), an X11 session or core dumps that could not be disabled now end the session (
refused after the profile became production), as they refuse a production profile at start-up; before, only the privilege audit was run again. -
The separated-setup handoff keeps
--hub-plain-http(#111). The console-account command printed (or run) forlocksql console --hub http://… --hub-plain-httpnow carries the flag; without it the console refused the http:// hub (lockkit v0.3.1). The console usage line lists the flag. -
Audit writes fail closed (lockkit #5). An approved or auto-approved query whose approval record cannot be written runs nothing: the agent gets
CodeInternal("the console could not record the approval in its audit log"), the human sees why; three failed writes in a row end the session. Denials stay denials. -
Widenings take effect only once recorded (#102). Auto mode on, clear results shown,
:unmaskallowed and a loosened policy approved (at:reviewor at start) write their audit record first; if it cannot be written they stay off (the policy is neither saved nor applied, and the console does not start), and the console says so. Narrowing still applies at once. -
No widening switch while a prompt waits. Shift+Tab (auto mode on), Ctrl-O (clear results on) and
:unmaskare refused, with a line saying to answer the prompt first, while the console waits for an approval, a typed value, a refusal to acknowledge or a reconnection; switching auto mode or clear results off stays allowed. At level 2 a second finger prompt no longer nests in the pending one (lockkit #2). -
DISTINCTover a PII column is counted. LikeGROUP BY, aSELECT DISTINCTthat outputs a PII column now runs the k-anonymity row count of itsFROM/WHERE, and warns underk. In a correlated subquery, a recursive CTE, aSELECTwithoutFROMor a write, such aDISTINCTis now refused, as a grouping already was. -
MINandMAXof a PII column are redacted like other aggregates. Their result is now plain<redacted>in every mode: nopartialoremailmask, no cell reference, and it cannot be compared or joined.MIN/MAXof an unmasked column andANY_VALUEare unchanged.
v0.9.0 (2026-10-28) #
Changed #
- One
locksqlcommand, backed by Docker.scripts/install.shnow installs thelocksqllauncher, a shell script, instead of a binary: every command,locksql console,locksql version,locksql mcpand the others, runs in the release imageghcr.io/davidgodefroid/locksql:X.Y.Z(-vaultwith the vault CLI,LOCKSQL_VAULT=1), with the account and the volumes it needs (console aslocksql,init/profileas you,doctor --fixas root, the rest asagent). Thelocksql mcpthatlocksql initwrites now works as is: theclaude mcp add ... sh -c 'exec docker run ...'step is gone (such entries keep working).locksql versionprints the launcher's and the image's versions;locksql updatereplaces the launcher (signature and checksum checked) and pulls the image. The image's cosign signature is verified at install and update, and the launcher runs it by that verified digest.LOCKSQL_IMAGEruns an image of your own,LOCKSQL_DOCKER_ARGSaddsdocker runarguments. The release publishes the image, signed with cosign, and no longer the.deb,.rpmand.apkpackages of the binary; the archives keep the binary, for an agent that runs in a container of its own, and now hold the launcher. - The launcher's console is confined and per project. It runs with no
capability, no privilege gain, a read-only root with a small
/tmp, at most 256 processes and 1 GB, in a container of its own per project (locksql-console-<n>,LOCKSQL_CONSOLE_NAMEto name it). An explicit--project DIRofconsoleorprofileis the project mounted.LOCKSQL_READONLY_PROJECT=1mounts the project read-only but for.locksql/, which on Linux the console checks by device and inode against what the launcher checked (lea runs its console this way).VAULT_CACERTreaches the console with its file. Release signatures (archive and images) are bound to the exact tag installed.
Removed #
- The host install. The binary refuses
locksql install,locksql setupand its ownlocksql update("locksql runs in Docker"), and barelocksqlprints the help instead of leading to the host setup. Migrating from a host install: run the newinstall.sh, which replaces the binary with the launcher; whatsudo locksql installcreated (thelocksqlaccount andlocksql-clientsgroup,/etc/locksql/, the socket directory and/etc/tmpfiles.d/locksql.conf) stays until you remove it (docs/docker.md). The code goes in a later release.
Added #
locksql config export/locksql config import. Share profiles and PII rule sets with a team as one versioned JSON file. The export holds no secret, nouser(unless--include-user) and no file of one machine (tls_ca,tls_cert,tls_key,ssh.keyare named inneeds_localand given on import with--local). The import is strict, validates the merged result before writing, refuses any loosening without--allow-loosen(the console still asks at its next start), keeps*.bakbackups and writes aconfig_importaudit record. See docs/usage.md, "Share a configuration with your team".- Wildcards inside a PII rule segment. A
*may now start or end a segment of adb.table.columnrule, so*.*.*email*coverscontact_email,email_backupandEmail, andapp.user*.phonecovers every table whose name starts withuser. A*anywhere else in a segment is still refused. A new wildcard mask rule only tightens; a new wildcard allow rule is a loosening the human confirms in:review, as every added allow rule. - More column names in the startup PII scan. The scan now proposes
mail,e_mail,login(not a date or time column such aslast_login),username,nickname,displayname,pseudoandhandlefor masking, and asks about free-text columns (notes,comment,bio,remarks, ...) and coordinates (lat,lng,latitude,longitude) as quasi-identifiers ([y/N]).mail_id,login_countand other metadata columns stay out (lockkit#21, v0.2.1). write_functions = "allowlist". An opt-in profile setting that applies the read function allowlist to writes: every function anINSERT,REPLACE,UPDATEorDELETEcalls, at any depth, must be allowlisted (plus key generators such asgen_random_uuid,nextval,UUID,SYS_GUID,randomblob, MySQLVALUES(col)inON DUPLICATE KEY UPDATE), or the write is refused withE_FUNCTION. Schema-qualified calls, calls through quoted or non-ASCII names or with a blank before(,CALL,MERGEand writes the parser does not read are refused, and every column the write names is resolved against the catalog (PostgreSQL attribute notationt.fcallsf(t)). The default stays"denylist"; turning the allowlist off is a policy loosening confirmed in the console, and existing approved policies keep their fingerprint.- Databases that publish no port (#35). A profile can set
proxy_command, an argv whose stdin and stdout carry each database connection, as OpenSSH'sProxyCommand:docker exec,podman exec,kubectl exec, orssh ... socatto a server bound to its container's loopback or to a Unix socket on a remote host. It runs without a shell, as the console account, in an empty private directory, without a terminal and with a scrubbed environment, and may not name a file inside the project (symbolic links resolved, case folded on macOS and Windows) nor carry a secret (--password=,MYSQL_PWD=, mysql's-pSECRET); closing the connection ends the command and every process it started (a process group, a job object on Windows). The argv is part of the approved policy (any change loosens it), its stderr ends a connection error, andlocksql doctorchecks the program.docs/usage.mdgains "Databases that publish no port" with the recipes;docs/docker.mdshows how to add the client a recipe needs to the console's image, which ships none, and why not to mount the Docker socket there.
Changed #
- Function names are matched exactly. A function call must use an
unquoted name in ASCII letters, and in MySQL no blank between the name and
(, so that the allowlist checks the function the server calls:"lower"(x),`lower`(x), non-ASCII spellings and MySQLlower (x)are now refused withE_FUNCTION. Writelower(x). - Release supply chain pinned.
scripts/install.shchecks the cosign signature ofchecksums.txtagainst the release workflow run for a version tag (^https://github\.com/DavidGodefroid/locksql/\.github/workflows/release\.yml@refs/tags/v[0-9], aslocksql updatedoes), instead of any workflow or ref of the repository, and now requires cosign: without it the script stops, andLOCKSQL_SKIP_COSIGN=1installs without the signature check (the formerLOCKSQL_REQUIRE_COSIGNis gone).locksql installprints the sha256 of the binary it copies beside its prompt, and its root script checks its copy against it for every source, not only with--trust-binary.locksql update's package instructions check the signature and checksum beforeapt,dnforapk, and stop at the first failed step. The workflows' actions are pinned to commit SHAs, and the release job pins GoReleaser, cosign and syft. - PostgreSQL: allowlisted functions overloaded in the database are
refused. PostgreSQL picks the best match for an unqualified function
over every schema of the search path, so a
public.lower(varchar)runs instead ofpg_catalog.lower(text)for avarcharargument. Before a read (and a write underwrite_functions = "allowlist") is planned, and again just before it runs, the console looks up the functions and operators it uses and refuses it withE_FUNCTIONwhen one is also defined outsidepg_catalog. Those owned by an extension are not counted. - PII rules in named sets, one per profile by default. PII rules no
longer live in one
pii.tomlshared by every profile of a scope: each profile uses the set named by its newpii_ruleskey, by default its own name, kept inpii/<set>.tomlnext to the config. Profiles on unrelated servers no longer see each other's rules (an[[allow]]or a wildcard rule written for one product no longer reaches another); profiles on isomorphic databases (dev, uat, prod) share rules by naming the same set. Changingpii_rulesis a loosening, approved again at the next start. Upgrading: at the first start the existingpii.tomlis moved topii/default.toml(the setdefault), with a one-time notice; profiles approved before keep using it until they get apii_ruleskey, and any new profile starts with its own, empty set. The console warns at start when a profile withoutpii_rulesshares its set by accident (names that differ only by case, a name another profile uses as itspii_rules, or a profile nameddefault). Two consoles starting together no longer fail on the migration. - Docker is the only documented deployment. The README Quick start and
Install,
docs/usage.mdand the site now describe only the Docker setup: build the image,locksql init,locksql profile add,locksql doctor --fixas root in the image, wire the agent withdocker run ... locksql mcp, run the console container.docs/docker.mdis the setup guide, and for the stronger boundary it recommends running the agent in its own container without the Docker socket. The install script, packages,locksql install,locksql setup,locksql update, the desktop and session setups and WSL2 are no longer documented or supported; their code is unchanged. - Vault token asked at start (#51). With
credentials = "vault"the console asksVault token for <VAULT_ADDR>:(no echo) before the first Vault read, instead of relying on avault loginof the console account and its~/.vault-token. The token is passed to thevaultCLI asVAULT_TOKENin the child's environment only (never argv, disk, logs or the audit trail), kept in memory for the session so reconnects and lease renewals do not ask again, and wiped when the console ends. When Vault refuses a read (an expired or revoked token, or a policy that may not readvault_path: Vault does not tell which), the console saysVault refused the readand asks for a token again, mid-session too; three refusals in a row stop the connection. Needs lockkit v0.2.0. - Clear results refused at desktop security level 1. When the console
runs in a desktop terminal of the console account without a fingerprint
reader, the agent's account can read its window, so the clear results
would reach the agent.
locksql console --show-resultsis now a configuration error there (level 1 does not accept --show-results: the agent's account can read this window), raised before any prompt or file write, and Ctrl-O or:resultsleave the results masked withresults stay masked at level 1: the agent's account can read this window(nothing audited), as at level 2.--autoand--allow-unmaskare unchanged. Levels 0 (session shape, Docker) and 2 are unchanged. - More statements refused at every tier. The classifier's denylist
now also refuses PostgreSQL notification, logical decoding, replication
slot and origin, backup and recovery control and statistics reset or
override functions; MariaDB Spider, MySQL keyring, audit log, group
replication, version token and locking service functions and more
lib_mysqludf_sys names; SQLite
fsdirandzipfile. DDL that reaches the server's filesystem, another server or native code is refused too:DATA|INDEX DIRECTORY,ENGINE=FEDERATED|CONNECT|SPIDER,CREATE SERVER, foreign tables and data wrappers,CREATE TABLESPACE,ADD DATAFILE,LANGUAGE C,internaland untrusted languages,CREATE LANGUAGEand the SQLitefsdir,zipfileandcsvmodules. The security model now says the denylist is best-effort above tierread, where approval is the control. - Internal: locksql runs on lockkit. The product-neutral half of locksql moved to lockkit, shared with the other lock* tools (see CONTRIBUTING.md). No user-visible change: same commands, flags, messages, config keys, IPC protocol, audit lines, approved-policy fingerprints (no re-approval after the upgrade), exit codes, paths and wired agent files, each pinned by a golden test.
Fixed #
- Value detectors (#17). Unicode e-mail addresses
(
müller.hans@exämple.de) are masked whole; phone, IBAN, card and national-id values grouped with U+00A0, U+202F or U+2009, or written with full-width digits, are masked; integralfloat4/float8cells go through the detectors like integers. A new opt-in detector,phone_loose, masks national numbers without a trunk 0 ((415) 555-2671,612 345 678); the defaultphonerule is unchanged and now documented next to thedetectorssetting. Detectors only mask more than before (lockkit v0.2.0). .locksql/in Docker. The console account exists only in the image, so nothing gave it the project's.locksql/directory, and an agent running as your uid could replacepii.toml.locksql doctor --fixnow runs as root in the image (docker run --user 0 … locksql doctor --fix --yes, see docs/docker.md) withoutsudo, and its hint names the console account by uid, which the host can use.- Root repairs no longer follow symbolic links.
doctor --fixandsetupchanged.locksql/and its files by path as root; a symbolic link put there by the agent's account would have handed its target to the console account, or made it world-readable. The root script now enters the directory, checks its real path and changes.; a.locksqlthat is a link fails the check with no repair. Aconfig.tomlorpii.tomlthat is a symbolic or hard link is refused, with a message, rather than made readable.
v0.8.0 (2026-10-19) #
Added #
-
locksql profile add. The database profile step oflocksql setupalone, for a console in a container (docs/docker.md): it appends a profile to.locksql/config.tomlfrom its questions or its flags. In a container the wizard offershost.docker.internalas the host and never the keychain. -
The setup wizard asks for TLS and Vault. A remote host gets a
tlsquestion (defaultverify-full), and the password source is nowask,keychainorvaultwith its path (--tls,--credentials vault,--vault-path). -
A Docker image with the vault CLI.
docker build --target vaultadds the HashiCorp Vault CLI, checked against pinned sums, forcredentials = "vault". The default image stays without it. -
locksql update. Replaces/usr/local/bin/locksqlwith the latest release, or the one--versionnames, from your own account, asking forsudoonly for the final copy. Before that copy it checks the cosign signature ofchecksums.txtagainst this repository's release workflow (cosign is required: there is no unsigned path), the archive's checksum and the new binary's version. The root script checks a root-owned copy against the printed sha256 before installing it. A package's/usr/bin/locksql, or a binary root does not own alone, is refused with how to upgrade it. An older release needs--allow-downgrade. -
New-version notice.
locksql consoleandlocksql doctorprint on stderr when a newer release exists, with how to upgrade for the way locksql was installed. At most one request a day: a single GET ofreleases/latest, without added header or identifier, 2 s timeout, cached in$XDG_CACHE_HOME/locksql/update-check(0700/0600). Failures print nothing; the output and exit codes do not change. In a separated setup the console checks in the console account only.LOCKSQL_NO_UPDATE_CHECK=1turns it off, and is passed on through the console handoff.locksql mcpnever checks.
Upgrading #
- From this release on,
locksql updatereplaces runningscripts/install.shagain. Restart the console after an update.
v0.7.0 (2026-10-18) #
Added #
- Credentials from HashiCorp Vault.
credentials = "vault"withvault_path = "database/creds/<role>"(or a KV secret) makes the console read the database user and password withvault read -format=jsonat every connection, as the console account and with its Vault token. The login is kept in memory only, never in the keychain or on disk. Before a dynamic login's lease ends, the console reconnects with a fresh one, and the audit log names the user of each login. Errors tell apart a missing CLI, a missing or refused token, an invalid path and an invalid role.locksql doctorchecks the CLI and, in separated mode, fails when the agent's account holds a Vault token.
Upgrading #
- Nothing to do. Switching a profile to
credentials = "vault"or changing itsvault_pathis a loosening the console asks you to approve.
v0.6.0 (2026-10-14) #
Added #
- Approval mode in the audit log. Every approval record (a run, a
policy change, an unmask) now carries
approval: how the human approved. The existing separated setup writes"session", a console in a login session of the console account. - Groundwork for a desktop console. The console can now run in a
window of the console account on the agent user's Wayland desktop, with
approvals typed (
desktop+typed) or confirmed with a fingerprint read byfprintd(desktop+fingerprint). Nothing starts it yet: the launcher comes in a later release, and existing installs keep the session setup.system.tomlaccepts afingerkey, the fprintd name of the finger enrolled for the console account (for exampleleft-index-finger); an unknown name is refused.
Changed #
- The console checks its isolation before any prompt or state change. A production profile in an X11 session is now refused before the policy review: before, an approval could be saved and only then refused.
- The startup line of the separated setup reads
session mode: boundary: kernelinstead ofseparated mode: console account …, clients of group …, display ….
Upgrading #
- Nothing to do. A script that reads the console's startup line must match the new text.
v0.5.2 (2026-10-11) #
Changed #
- k-anonymity warns instead of refusing. A PII filter, group or
aggregate that covers fewer than
limits.k_anonymityrows is no longer refused after approval withE_K_ANON. The counts now run before the approval prompt, and each count under k adds a▲ k-anonymitywarning to the approval frame, just aboveApprove? [y/N]: the statement could single out one person or infer a masked value by cross-checking. You decide:yruns it,ndenies it as usual. In auto mode the statement runs, and the warning is printed in its frame and kept in the audit record'swarnings. A count that cannot run warns the same way. - The agent learns nothing of the counts: no refusal code, the same answer whether a count is under k or not, and in auto mode a response time levelled from the counts on. Row estimates stay hidden under a PII filter.
- The structural PII rules are unchanged: equality only on PII columns, no
LIKE, range or negation, masking, cell references and placeholders still refuse as before. E_K_ANONis retired. No refusal carries it; it stays a known code so thatlocksql statsstill counts it in older audit logs. The MCP server instructions and the agent instructions written bylocksql initno longer say that a k-anonymity refusal is final: rerunlocksql initto refresh them.- docs/security-model.md now describes k-anonymity as a decision aid for the human, not a guarantee.