mcp server
locksql
Your AI writes the query, you hold the key. A human-approved, PII-masking database gateway for AI agents (CLI + MCP).
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 26, 2026locksql puts a human-held console between your AI agent and MariaDB, MySQL, PostgreSQL or SQLite. The agent proposes SQL; the console, running in a terminal you keep in view, holds the credentials, parses and weighs every statement, masks personal data and waits for your approval. The agent never sees a credential and never opens a connection.
[!NOTE] Pre-release (
v0.8.x). See Install.
Why locksql?
Agent tools already ask before running a command, but that approval lives inside the agent's own process: the agent holds the credentials, the prompt can be configured away, and a long session trains you to press "yes". locksql moves the decision out of the agent.
| In-agent approval | locksql | |
|---|---|---|
| Agent never sees the DB password | ❌ | ✅ |
| Agent has no DB connection | ❌ | ✅ |
| Approval cannot be switched off from the agent side | ❌ | ✅ |
| Every read parsed down to its source columns | ❌ | ✅ |
EXPLAIN cost check before running | ❌ | ✅ |
| PII masked on its source column, not its label | ❌ | ✅ |
| k-anonymity warning on PII filters and aggregates | ❌ | ✅ |
| Search on a PII value the agent never sees | ❌ | ✅ |
| Policy loosening needs a human confirmation | ❌ | ✅ |
| Append-only audit log | ❌ | ✅ |
In-agent approval is a convenience; a separate approval process is a boundary.
Highlights
🔑 One key holder Credentials are typed at console start, read from the OS keychain or fetched from HashiCorp Vault. Never in argv, env, files, logs, the socket or error messages.
👤 Human in the loop
Every statement is shown with its plan, cost and PII footprint. On production
you type the profile name, not y.
🧱 Fail closed A dialect-aware classifier refuses anything it cannot classify with certainty. Reads are parsed in full; unknown syntax is refused.
🕶️ PII masking
Every output column is traced to its source through aliases, CTEs, unions and
joins, then masked (redact by default, partial or email).
⚖️ Weight check
LIMIT is mandatory, EXPLAIN runs first, and plans that would scan too many rows
are refused before they run.
🛡️ OS separation
locksql install runs the console under its own account; the kernel checks
every peer on the socket.
🔎 Blind search
The agent filters on '${email}' and you type the value in the console, or
on a redacted cell's reference ('${r1.2.3}'). The value never reaches it.
👁️ Clear for you only
Ctrl-O in the console prints the clear rows there while the agent gets
them masked; --allow-unmask (or :unmask in the console) is the only
way an agent can ask for raw PII.
🔐 Remote, verified
TLS verifies the server certificate by default (tls = "verify-full"), and an
ssh table reaches a database behind a bastion: the console opens the tunnel
itself, checks the bastion's host key and listens on no local port.
Quick start
A first approved query in three commands (CI runs this path on a fresh Ubuntu VM and in fresh Ubuntu and Fedora containers):
# 1. Install (see Install below for packages, checksums and signatures)
curl -fsSL https://raw.githubusercontent.com/DavidGodefroid/locksql/main/scripts/install.sh | sh
# 2. In your project: the console account, your agents, a database profile
locksql setup
# 3. In a session of the locksql account (switch user, or a text console
# with Ctrl-Alt-F3), start the console it printed and keep it open
locksql console --project DIR
Then ask your agent, anywhere in the project: "how many orders were placed
yesterday?". Each query waits for your y in the console.
locksql setup wires every installed agent (Claude Code, Codex, Gemini CLI,
Cursor) for your account, creates the locksql console account with
sudo once you confirm, asks for the database profile (engine, host, user,
where the password comes from: typed, the console account's keychain or
Vault; never the password itself) and writes it to the project's .locksql/config.toml,
gives the console account access to the project, and prints the command of
step 3. Log out and in again after the first setup: your account joins the
locksql-clients group. On macOS, setup prints the root commands for an
administrator to run, then you run setup again.
The console runs in the locksql account's own session, never in a
terminal of your account, where the agents run and could read what you
type. Run from your account, locksql console therefore prints the
command for that session instead of starting. When your agents run under
an account of their own, locksql console from yours opens the console as
locksql through sudo -u, machinectl shell or su -l.
locksql doctor checks the setup from either account, and
locksql doctor --fix repairs what it safely can (permissions, the socket
directory, the project's access, the agents' wiring). Bare locksql
re-wires agents installed since; in the service account it starts the
console. The details are in docs/usage.md.
How it works
Two processes in two terminals. Only the console holds the key.
sequenceDiagram
autonumber
participant A as 🤖 AI agent<br/>(locksql mcp / CLI)
participant C as 🔐 locksql console<br/>(your terminal)
actor H as 👤 You
participant D as 🗄️ Database
A->>C: plan "SELECT … LIMIT 20"
C->>C: classify · parse · resolve PII columns
C->>D: EXPLAIN
D-->>C: plan & row estimate
C-->>A: plan id + verdict (OK / WARN / REFUSE)
A->>C: run PLAN_ID
C->>D: k-anonymity counts
C->>H: approval screen (warns of a count under k)
H-->>C: y
C->>D: the statement
D-->>C: rows
C-->>A: masked, capped rows (untrusted data)
-
The agent submits SQL with
locksql plan(or thelocksql_planMCP tool). The console classifies it, parses and analyses a read down to the source column of every output, runsEXPLAIN, weighs the plan and returns a one-shot plan id with a verdict (OK,WARNorREFUSE). -
The agent calls
locksql run PLAN_ID. When the statement filters, groups or aggregates PII, the console first runs its k-anonymity counts. It shows you the approval screen, with a▲ k-anonymitywarning above the prompt for each count underk_anonymity, and waits:╭─ DEV · 127.0.0.1 / app · user alice · tier read ───── │ requested by uid 1000 (alice) · pid 48211 (claude) │ agent: list the Belgian customers to check the email campaign │ │ SELECT id, email FROM customers WHERE country = 'BE' LIMIT 20 │ │ class READ · EXPLAIN: customers range ~410 · est. 410 rows examined · verdict OK │ reads: app.customers │ returns at most 20 rows │ PII columns touched: customers.email (select) │ masked outputs: email → redact │ PII: masked (4 column rules; detectors: email, phone, iban, card) ╰─ Approve? [y/N] -
On approval the console runs the statement, masks PII, caps the output and hands the rows back to the agent, framed as untrusted data.
Your terminal Agent side
┌───────────────────────────────────┐ ┌──────────────────────────────┐
│ locksql console --profile dev │ local │ locksql mcp (stdio MCP) │◄── AI agent
│ credentials (ask | OS keychain) │ socket │ locksql plan|run|status|pii │◄── AI via shell
│ policy · classifier · EXPLAIN │◄──────────►│ no secrets, no DB connection │
│ approval prompt · masking · audit│ JSON-RPC └──────────────────────────────┘
└─────────────────┬─────────────────┘
│ native protocol
▼
MariaDB · MySQL · PostgreSQL · SQLite
Full walkthrough: docs/usage.md.
Engines
| Engine | Versions | Read-only session (tier read) | Server-side timeout | Plan source |
|---|---|---|---|---|
mariadb | 10.1+ (tested 10.11, 11.4) | SET SESSION TRANSACTION READ ONLY + START TRANSACTION READ ONLY | max_statement_time + KILL QUERY | EXPLAIN FORMAT=JSON |
mysql | 8.0+ (tested 8.0, 8.4) | same as MariaDB | max_execution_time (SELECT only; writes rely on KILL QUERY) + KILL QUERY | EXPLAIN FORMAT=JSON |
postgres | 13+ (tested 13, 17) | SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY + BEGIN READ ONLY … ROLLBACK | statement_timeout + cancel request | EXPLAIN (FORMAT JSON, VERBOSE) |
sqlite | 3 (pure Go, in-process) | mode=ro + query_only | interrupt on deadline | EXPLAIN QUERY PLAN + sqlite_stat1 (run ANALYZE once, or every full scan is a WARN of unknown size) |
All engines are pure Go: the binary is built with CGO_ENABLED=0. locksql
supports Linux and macOS, on amd64 and arm64. On Windows it runs inside
WSL2 (guide); native Windows is not supported.
Safety model
The console is the enforcement point. Skills, rules and MCP tool descriptions only guide the agent; the console's checks are the guarantee.
- One key holder. Credentials are typed at console start (no echo), read
from the OS keychain, or fetched from HashiCorp Vault with the
vaultCLI at every connection (credentials = "vault", suited to the database secrets engine's per-user, short-lived logins). They never appear in argv, environment, files, logs, the socket protocol or error messages. The console disables core dumps.credentials_ttlmakes it ask again after a set time; a Vault lease is renewed with a fresh login before it ends. - OS separation.
locksql installcreates alocksqlconsole account and alocksql-clientsgroup: the console runs as that account in its own login session, the agent's account only reaches its socket, and the kernel checks every peer. The console refuses to start without this setup, since an agent in the console's own account could read or type into its terminal. - Human approval. Every statement is shown and approved in the console
terminal; no socket method can approve. On a production profile you type the
profile name, not
y. Pending keystrokes are flushed before each prompt, so type-ahead never approves. No answer within 5 minutes means denied. You can deny with a reason the agent reads (n use the orders table). - Fail closed. A dialect-aware classifier refuses anything it cannot
classify with certainty: several statements, comments, variables, bind
parameters, file and OS access, sleeps and locks, session tampering. Reads
are limited to
SELECT,WITH ... SELECTandEXPLAIN SELECT, parsed in full: unknown syntax, functions outside an allowlist and system schemas are refused. - Tiers.
read < write < ddl < admin; the default isread. Tierreadis also enforced server side (read-only session and transaction). - Weight check. A READ statement must carry
LIMIT nwithn <= max_rows. The console runsEXPLAINand refuses statements that would examine too many rows (or cost more thanexplain_cost_refuse); a recursive CTE is at least WARN on PostgreSQL and SQLite, whose planners only guess how often it iterates; REFUSE cannot be overridden from the console. - PII masking. At every start the console scans the schema and proposes
rules for columns that look like personal data (multilingual names and
types) and that no rule names yet. Every output column is resolved to its
source columns through aliases, functions, subqueries, CTEs, unions, joins
and
*, and masked on that source, not on its label; the engine's origin metadata is a second check, and a result whose columns do not match the analysis is dropped. Value detectors (email, phone, IBAN, card, opt-in national ids) mask the other cells. Mask modes:redact(the default),partial,email. Quasi-identifiers (birth date, postal code, gender) are never masked unless you accept them one by one, since masking blocks range filters,LIKEandORDER BYon the column. - Statement text. While mask rules exist, the views that hold the text of
past statements are refused, in reads and in writes:
pg_stat_statementsandpg_stat_activity; on MySQL and MariaDB,information_schema.PROCESSLIST, everyperformance_schemaandsysrelation,mysql.general_logandmysql.slow_log. - PII usage. PII columns may be selected, counted, joined with
=and filtered with=,IN (literals)orIS NULL. Expressions over them,LIKE, ranges andORDER BYare refused. A filter, grouping or aggregate on PII that covers fewer thank_anonymityrows (default 5, production 10) warns you on the approval screen, since it could single out one person; you decide. In auto mode it runs, and the warning goes to the console and the audit log. The agent is told nothing of the count. - Placeholders and references. To filter a PII column on a value the agent
does not know, it writes
col = '${email}'and the human types the value in the console (no echo); the value never reaches the agent, the audit log or a client error. Redacted cells come back as<redacted:r1.2.3>(result, row, column), and the agent filters on one withcol = '${r1.2.3}'; the console keeps the clear values of the last results in memory only. Both work only compared with a PII column, and skip the k-anonymity count, since the agent did not choose the value. The approval screen warns, in red and in the audit log, when the agent put a clear value in its statement, when a reference is combined with a unique-key filter or an output with no plain column, and when one result's cells are filtered on one by one beyondlimits.reference_probe. Warnings never refuse and never carry a value. - Quiet failures. Clients get a generic message, never the server's error
text, and no timings; once the statement is approved,
query.runanswers on a 250 ms quantum (a refusal before approval is not quantized). - The AI tightens, the human loosens. Agents may add mask rules and request changes. A config edit that loosens the policy (higher tier, larger limits, new host, removed PII rule, ...) only takes effect after you confirm it in the console.
- Audit. Every login, policy change, refusal, decision, run, catalog read and logout is appended to a JSONL audit log (mode 0600): the approval before the statement runs, its outcome after. The records of a client request name the requester: its uid, pid and account. Never secrets, never row data.
Details and threat model: docs/security-model.md.
Configuration
Profiles live in .locksql/config.toml at the project root (found by walking
up from the current directory) and in <user config dir>/locksql/config.toml.
A project profile wins over a user profile of the same name. No file ever
holds a secret: keys named like password, passwd, pwd, secret or
token, and DSNs with an embedded password, are refused.
Breaking change. A remote profile with no
tlssetting now verifies the server certificate (tls = "verify-full"). A server with a private CA needstls_ca; to keep encryption without verification, settls = "require". Until then its connection fails with an error that names both settings.
[profiles.uat]
engine = "mariadb" # mariadb | mysql | postgres | sqlite
host = "db.uat.example.com" # or path = "app.db" for sqlite
port = 3306 # default 3306, or 5432 for postgres
user = "" # empty: asked at console start
database = "" # empty: chosen per query (--db)
credentials = "ask" # ask | keychain | vault
tls = "verify-full" # disable | prefer | require | verify-ca | verify-full
tls_ca = "" # optional PEM bundle replacing the system roots
credentials_ttl = "20m" # optional: ask for the secret again
tier = "read" # read | write | ddl | admin
production = false
detectors = ["email", "phone", "iban", "card", "be_niss"]
[profiles.uat.limits]
statement_timeout = "30s"
explain_rows_warn = 100000
explain_rows_refuse = 1000000
max_rows = 200
max_cell_chars = 200
max_output_bytes = 65536
k_anonymity = 5
explain_cost_refuse = 0 # engine cost units; 0 = off
idle_timeout = "20m" # the session ends after this long idle
max_session = "4h" # ... and after this long in all
With credentials = "vault" the console runs vault read -format=json <vault_path> at every connection, as the console account and with its
environment (VAULT_ADDR, its token from vault login), and connects with
the username and password of the secret. Nothing is stored. See
Credentials from HashiCorp Vault.
[profiles.reporting]
engine = "postgres"
host = "db.internal"
tls = "verify-full"
credentials = "vault"
vault_path = "database/creds/reporting-ro" # or a KV secret: "secret/data/reporting"
A database reachable only from a bastion gets an ssh table: the console
opens the SSH connection itself, with no local listening port, and verifies
the bastion against the console account's ~/.ssh/known_hosts (asking on
first use, refusing a changed key). host is then the database as seen from
the bastion; keep tls = "verify-full" unless the database runs on the
bastion itself. See
Reaching a remote server through SSH.
[profiles.prod]
engine = "postgres"
host = "db.internal" # as seen from the bastion
tls = "verify-full"
production = true
[profiles.prod.ssh]
host = "bastion.example.com"
port = 22 # default 22
user = "deploy"
auth = "key" # key | agent | password
key = "~/.ssh/id_ed25519" # auth = "key" only; the console account's home
credentials = "ask" # passphrase or SSH password; default: the profile's
| Key | Default | Notes |
|---|---|---|
engine | required | mariadb, mysql, postgres, sqlite |
host, port | required host; port 3306 / 5432 | a host starting with / is a Unix socket (MariaDB/MySQL) |
path | required for sqlite | relative to the project root; the file is never created |
user | asked at start | with credentials = "vault": the secret's username; set it only for a secret without one |
database | none | the only database served (and scanned for PII); empty: every database the account can see, chosen per query with --db |
credentials | ask | keychain offers to save the secret after the first successful login; locksql forget --profile P removes it. vault reads the login from vault_path at every connection |
vault_path | required with credentials = "vault" | the Vault path read with vault read, such as database/creds/<role> or secret/data/<name>; any change is a loosening |
credentials_ttl | none | a duration of at least 1m ("20m", "1h"): the connection is closed and the secret asked again once it is that old |
tier | read | highest statement class allowed |
production | false | stricter limits, typed approval, auto mode unavailable |
detectors | email, phone, iban, card | also be_niss, fr_nir, nl_bsn, us_ssn; [] disables them |
limits.statement_timeout | 30s (production 10s) | |
limits.explain_rows_warn | 100 000 (production 20 000) | |
limits.explain_rows_refuse | 1 000 000 (production 200 000) | |
limits.max_rows | 200 | upper bound for LIMIT n and for returned rows |
limits.max_cell_chars | 200 | longer cells are cut with … |
limits.max_output_bytes | 65 536 | output is cut with a marker |
limits.k_anonymity | 5 (production 10) | a PII filter, a group or an aggregate of a PII column covering fewer rows warns on the approval screen (it no longer refuses); lowering it is a loosening |
limits.reference_probe | 5 | distinct cells of one result the agent may filter on one by one before the console warns (an IN list counts once); raising it is a loosening |
limits.idle_timeout | 20m (production 10m), or max_session when that is shorter | the console session ends after this long without activity; at least 1m and at most max_session; raising it is a loosening |
limits.max_session | 4h (production 2h) | the console session ends after this long in all; at most 24h; raising it is a loosening |
limits.explain_cost_refuse | 0 (off) | refuse plans above this total cost, in the engine's own units; SQLite reports no cost and is not checked |
ssh.host, ssh.port, ssh.user | required host and user; port 22 | the bastion; not with sqlite or a Unix socket host |
ssh.auth | required | key, agent (SSH_AUTH_SOCK of the console) or password |
ssh.key | required with auth = "key" | private key of the console account, mode 0600 |
ssh.credentials | the profile's credentials (ask when that is vault) | ask or keychain for the passphrase or SSH password; locksql forget --profile P removes it too |
PII rules
PII column rules live in .locksql/pii.toml inside a project, and in
<user config dir>/locksql/pii.toml (~/.config/locksql/pii.toml on Linux)
elsewhere:
[[mask]]
column = "app.users.email" # db.table.column, * per segment
mode = "email" # j***@example.com
[[mask]]
column = "app.users.customer_ref"
mode = "partial" # j***(12)
[[mask]]
column = "*.*.recipient_reference" # <redacted:rN.R.C> (default)
[[allow]] # explicit exception: never mask
column = "app.templates.name"
| Mode | Output | Notes |
|---|---|---|
redact (default) | <redacted:rN.R.C> (result, row, column: a reference the agent can filter on; plain <redacted> if the console cannot hold the cell, or for any column but a plain one whose every source is under a mask rule); partial, email and detector-masked cells carry no reference | a rule without mode is redact |
partial | j***(12) | first character and length |
email | j***@example.com | first character and domain; other values as partial |
A column covered by several rules with different modes is redacted. Changing
a mode is a loosening unless the new mode is redact.
mode = "hash" is no longer a mode: a rules file that uses it is rejected
(unknown mode "hash" (want redact, partial or email)).
On PostgreSQL the first segment is the schema (public.users.email).
--allow-unmask
An agent can ask for unmasked PII output (locksql plan --unmask, or
unmask in the MCP locksql_plan tool) only when the human allowed it in
the console: locksql console --allow-unmask at start, or :unmask while
it runs (confirmed with y; :unmask again turns it off). Without it, the
console refuses every unmask request before anything runs and audits the
refusal. Like auto mode, it is console-only: no client or config file can
turn it on, and each change is audited (unmask record). With it, each unmasked query still needs the human's
approval, shown as PII: UNMASKED in red, and is never auto-approved. The
console then prints the clear result, so you see exactly what the agent
received (the audit log still holds no row data).
Console keys and status line
The console runs its own line editor, so two keys and one command switch
its behaviour while it runs, without a restart, and a status row fixed at
the bottom of the terminal always shows the state: the profile and tier, the approval mode, whether results
are shown in clear, unmask allowed, the open plans and what awaits
:review. The switches are console-only (no client, config file or
environment variable can set them) and each change is audited.
| Key | Typed | Effect |
|---|---|---|
| Shift+Tab | :mode | manual ⇄ auto approval mode |
| Ctrl-O | :results | results masked ⇄ shown here in clear |
:unmask | unmasked output off ⇄ allowed (allowing asks for y) |
Clear results (Ctrl-O)
Ctrl-O (or locksql console --show-results to start that way) prints in
the console, in clear, the result of each masked query it runs, as an
aligned table, so you see who the rows are while the agent still gets the
masked cells. The cells the agent got masked are yellow and carry their
reference (‹r1.1.2›); the output caps apply as for the agent. Off by
default. The console warns when it goes on (in red on production), the
status row reads results clear, every approval screen says result: shown here in clear, and locksql status reports show results on. The
audit log still holds no row data. Unavailable when the console runs in a
desktop terminal (security levels 1 and 2): the agent's account can read
that window, so --show-results is refused and Ctrl-O leaves the results
masked.
Approval mode (Shift+Tab)
The console starts in manual mode: every statement is prompted.
Shift+Tab (or locksql console --auto to start that way) switches to
auto mode, where statements that the tier and the weight check allow
run without a prompt. It exists for fast local iteration, and it is
deliberately narrow:
- it switches only from the console's keyboard; no client or config file can turn it on;
- it is unavailable on production profiles (the key says so and the console prompts as usual);
- it never auto-approves an unmasked query and never overrides a REFUSE verdict;
- the status row reads
mode AUTO, each auto-approved frame ends withauto-approved, and each auto-approval is audited withdecision: "auto"; the switch itself is audited asmode.
Limitations
- The console runs only in a separate account.
locksql setup(orsudo locksql install) sets it up;locksql consolerefuses to start without it and nameslocksql doctor. Starting it takes a session of that account (switch user, or a text console), since the agent's account can read its own terminals. Runlocksql doctorto see what your machine allows. Malware running as the console account, or as root, is out of scope. - k-anonymity is a decision aid, not a guarantee. A count under
kwarns you before you approve; it refuses nothing, and in auto mode the statement runs. Even heeded, it is a query-set-size control: it does not stop differencing attacks that combine several approved queries. - Placeholders leave room for a patient agent. Equality probing through
cell references only raises a warning: an agent that spreads its probes
under
limits.reference_probe, across results or sessions, is not stopped. The unmasked columns of a targeted row still identify it, and free text without a detector is not masked. - Clear results stay on the console's screen. With the clear results on
(Ctrl-O,
--show-results) and for an unmasked run, the clear rows stay in the scrollback of the console's terminal, visible to anyone who sees that screen (screen sharing, recording, a terminal that logs its output). locksql does not clear it. - Transport security depends on
tls. A remote profile defaults totls = "verify-full";preferis the default only for a Unix socket or a loopback host.production = truerefusesdisableandpreferon a remote host. Belowverify-caan active attacker on the path can intercept or downgrade the connection and read the traffic, but is not handed the password: a remote MariaDB/MySQL connection whose certificate is not verified (or plain TCP) refusesmysql_clear_password,sha256_passwordand acaching_sha2_passwordfull authentication, so a MySQL 8 account logs in that way only while the server's authentication cache holds it; PostgreSQL refuses a clear-text or MD5 password request (require_auth=!password,!md5). Each refusal names the remedy,tls = "verify-full"(withtls_cafor a private CA).verify-caandverify-fullprevent the impersonation;tls_cert/tls_keyadd a client certificate. Through ansshbastion the SSH leg is encrypted and verified, but the bastion-to-database leg is protected only bytls, unless the database runs on the bastion. See Transport security. - One console per profile and project, one request at a time. No remote or shared consoles, no data export.
Documentation
| 📘 docs/usage.md | console, client commands, MCP server, agent integration |
| 🐳 docs/docker.md | running the console and the agent's MCP server in Docker, with nothing installed |
| 🛡️ docs/security-model.md | threat model and mitigations, and an upstream recommendation (PII encrypted at rest with a blind index) |
| 🔍 docs/pii-analysis.md | how the PII analysis works: parser, provenance, usage rules, k-anonymity counts, masking (for contributors) |
| 🏁 docs/1.0.md | the 1.0 criteria and the versioning policy from 1.0 |
| 🚨 SECURITY.md | reporting a vulnerability |
| 🧑💻 CONTRIBUTING.md | building and testing |
Install
Every release ships a static binary for Linux and macOS (amd64, arm64),
Linux packages, a checksums.txt and its cosign signature. No Go toolchain is
needed.
Script (Linux, macOS). Downloads the archive for your OS and architecture,
checks it against checksums.txt (and the signature when cosign is on
PATH), then installs /usr/local/bin/locksql, with sudo if needed:
curl -fsSLO https://raw.githubusercontent.com/DavidGodefroid/locksql/main/scripts/install.sh
less install.sh # read it first
sh install.sh # LOCKSQL_VERSION=v0.8.0 to pin a version
LOCKSQL_INSTALL_DIR=~/.local/bin installs without sudo, but the binary is
then owned by your account and locksql doctor warns: in separated mode, the
console must run a binary the agent cannot replace (sudo locksql install
copies it to /usr/local/bin).
Upgrade. Run locksql update from your own account, without sudo: it
replaces /usr/local/bin/locksql with the latest release (--version vX.Y.Z
for another) once the release's cosign signature, its checksum and the new
binary's version check out, and asks for sudo only for that copy. It needs
cosign on PATH and a binary installed as above, owned by root; packages
upgrade with their package manager. Restart the console afterwards. See
docs/usage.md.
locksql console and locksql doctor print on stderr when a newer release
exists (locksql vX available (you run vY): locksql update, or the package
or download instructions for other installs). They check at most once a day,
with a single GET of https://github.com/DavidGodefroid/locksql/releases/latest
(no added header or identifier, 2 s timeout), cached in
$XDG_CACHE_HOME/locksql/update-check; a failure prints nothing.
LOCKSQL_NO_UPDATE_CHECK=1 turns it off. locksql mcp and the other
commands never check.
Debian, Ubuntu, Fedora, RHEL, Alpine. Packages install
/usr/bin/locksql, owned by root. Copy-paste, the latest version and your
architecture are detected:
V=$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/DavidGodefroid/locksql/releases/latest); V=${V##*/v}
A=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
U=https://github.com/DavidGodefroid/locksql/releases/download/v$V/locksql_${V}_linux_$A
# Debian, Ubuntu
curl -fsSLO $U.deb && sudo apt install ./locksql_${V}_linux_$A.deb
# Fedora, RHEL
sudo dnf install $U.rpm
# Alpine
curl -fsSLO $U.apk && sudo apk add --allow-untrusted ./locksql_${V}_linux_$A.apk
To upgrade, run the same lines again; to remove, sudo apt remove locksql
(or dnf remove, apk del).
By hand. Download locksql_<version>_<os>_<arch>.tar.gz, checksums.txt
and checksums.txt.sigstore.json, then:
cosign verify-blob --bundle checksums.txt.sigstore.json \
--certificate-identity-regexp '^https://github.com/DavidGodefroid/locksql/' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
checksums.txt
sha256sum --ignore-missing -c checksums.txt # macOS: shasum -a 256 --ignore-missing -c checksums.txt
tar -xzf locksql_<version>_<os>_<arch>.tar.gz locksql
sudo install -m 0755 locksql /usr/local/bin/locksql
From source. go install github.com/DavidGodefroid/locksql/cmd/locksql@latest
(Go 1.26 or later), then sudo locksql install --trust-binary.
Docker. Nothing to install but Docker: the image holds the separated setup, the console runs in one container and the agent's MCP server in another. Read what Docker does and does not separate first: docs/docker.md.
docker build -t locksql https://github.com/DavidGodefroid/locksql.git
docker build --target vault -t locksql https://github.com/DavidGodefroid/locksql.git # with the vault CLI
locksql profile add then writes a first database profile from its
questions. lea does all of it in
lea init and links a locksql command that opens the console.
Build
CGO_ENABLED=0 go build ./cmd/locksql
Requires Go 1.26 or later.