lockmq puts a human-held console between your AI agent and RabbitMQ. The
agent asks; the console, running in a terminal you keep in view under its own
account, holds the broker credentials and the only broker connections, serves
an allowlist of read-only operations, masks personal data in messages and
waits for your approval before any message content leaves it. The agent
never sees a credential and never opens a connection.
[!NOTE]
Pre-release (v0.6.x). The CLI, the MCP tools and the config files may
still change before v1. See Install.
Why lockmq?
"Why does orders.failed fill up?" is a good question for an agent, and
answering it means looking at queues, bindings, policies, consumers and, in
the end, a few messages. Handing the agent rabbitmqctl, the management API
or an admin password gives it every write too (purge, delete, publish,
shovels), and every message body in clear. lockmq moves the key and the
decision out of the agent.
Agent with broker credentials
lockmq
Agent never sees the broker password
❌
✅
Agent has no broker connection
❌
✅
Only an allowlist of read operations exists
❌
✅
Every message read approved by a human
❌
✅
Message bodies, headers and properties masked
❌
✅
Stream reads never store an offset
❌
✅
Queue peek off by default, never auto-approved, refused near the delivery limit
❌
✅
Policy loosening needs a human confirmation
❌
✅
Append-only audit log (never message content)
❌
✅
Highlights
🔑 One key holder
Credentials are typed at console start or fetched
from HashiCorp Vault (the RabbitMQ secrets engine works as is). Never in argv,
env, files, logs, the socket or error messages; URI passwords are stripped
from every answer.
📋 Catalogue first, no prompt
Overview and alarms, queues (paged, filtered, sorted), one queue with its
arguments, DLX, TTL, delivery limit, policy, bindings and consumers;
exchanges, policies, connections, channels, stream offsets and consumer lag.
👤 Messages, approved read reads a stream from first, last, an offset or a timestamp with an
unnamed consumer: nothing stored, nothing moved. You approve each read; on
production you type the profile name.
🧪 Peek, only if you allow it
Reading a classic or quorum queue requeues messages. It exists only on a
profile you set to tier peek, is approved every time, never auto-approved,
and refused near the quorum queue's delivery limit.
🕶️ Masking for messages
JSON bodies masked per leaf (redact-unknown on production), headers and
properties by rule and name, value detectors, <redacted:rN.K> references;
binary and compressed bodies reduced to size and hash.
🛡️ OS separation
The Docker image runs the console under its own account; the kernel checks
every peer on the socket, and the console refuses to start without it.
⚠️ An agent that can run docker (the default MCP wiring is a docker run)
can attach to the console: see
what Docker does and does not separate.
⚖️ The AI tightens, the human loosens
The agent may add mask rules or tighten a profile; any loosening (a tier, a
limit, a vhost, a URL) waits for your confirmation in the console.
🏷️ Refusals the agent can act on
Every refusal carries a stable code, a hint and a retry rule: rewrite, ask
the user, or stop.
Quick start
Docker only: one lockmq command, the launcher, runs every command in the
signed release image. The full guide is docs/docker.md.
The shortest path is the Lock Family's: install it with Homebrew, then let
lox add lockmq
ask where your broker is, set up its lock and start its console:
brew trust davidgodefroid/tap # Homebrew asks you to trust a third-party tap once
brew install davidgodefroid/tap/lockfamily # or only this tool: brew install davidgodefroid/tap/lockmq
cd ~/projects # with lea: its projects directory; with claude: the project
lox add lockmq # asks about the broker, sets up the lock, runs lox up
lea # or: claude, then lox approve
If a lea container already runs for the project, lox add says lea gets lock NAME at its next start: lea stop, then lea: lea mounts the locks' sockets
when its container starts, so lea stop, then lea brings the new lock in.
By hand, with lockmq's own installer:
# 1. Install the launcher (cosign checks the release and its image)
V=vX.Y.Z
curl -fsSLO "https://raw.githubusercontent.com/DavidGodefroid/lockmq/$V/scripts/install.sh"
less install.sh && LOCKMQ_VERSION=$V sh install.sh
# 2. In your project: the agent files, a broker profile, then a check of the setup
lockmq init claude
lockmq profile add # asks; or --non-interactive --url URL --user USER ...
lockmq doctor --fix --yes
# 3. Declare the lock and start it with its hub (lockhub's lox CLI)
lox add lockmq events
lox up
lox attach events # the console's own questions: policy review, password (detach: Ctrl-P Ctrl-Q)
# 4. Approve the agent's message reads
lox approve
The console runs hub-attached only: lox up starts it with its hub flags,
and lockmq console on its own refuses to start.
lockmq init claude wires Claude Code to lockmq mcp, which the launcher
runs in a throwaway container.
init, profile add and doctor are the same three verbs in every lock
of the family, which lox add drives (profile add --non-interactive --json, init claude, doctor --fix --yes --json); no form of profile add takes or asks for a password.
Inside the containers localhost is the container itself: profile add offers
host.docker.internal for a broker on your machine, and a broker container
is reached by its name on a network shared with the console. Then ask your
agent: "which queues have no consumer?", "why does orders.failed fill up?".
Catalogue calls answer at once; message reads wait for your approval on
lockhub (lox approve).
How it works
Two processes in two accounts. Only the console holds the key.
sequenceDiagram
autonumber
participant A as 🤖 AI agent<br/>(lockmq mcp / CLI)
participant C as 🔐 lockmq console<br/>(lockmq account)
participant L as 🏛️ lockhub
actor H as 👤 You
participant B as 🐇 RabbitMQ
A->>C: lockmq_queues (catalogue)
C->>B: GET /api/queues/{vhost}
B-->>C: queues
C-->>A: selected fields (untrusted data)
A->>C: lockmq_read order-events from last
C->>B: stream metadata
C->>L: signed ticket, masked screen
C-->>A: pending (ticket)
L->>H: approval screen (lox approve)
H-->>L: approve
A->>C: lockmq_read --ticket
C->>L: verify the decision
C->>B: unnamed stream consumer, at most read_max
B-->>C: messages
C-->>A: masked, capped messages (untrusted data)
The agent calls an MCP tool (lockmq_queues, lockmq_read, ...) or the
CLI. Both talk to the console of the profile over a Unix socket that only
the clients group reaches; the kernel's peer credentials are checked on
every connection.
The console checks that the operation exists, the vhost is served, the
tier allows it and the arguments are in range, before any broker call.
Catalogue operations run at once (audited). read and peek show an
approval screen first:
╭─ PROD · rabbitmq.example.com:15671 / orders · user lockmq-ro · tier peek ─────
│ requested by uid 1000 (alice) · pid 48211 · process "lockmq" (self-reported name)
│ agent: see why order events fail in the dead-letter queue
│
│ PEEK orders.failed count 3
│
│ PII: masked (2 mask rules, 1 allow rules; body redact-unknown; detectors: card, email, iban, phone)
│ ▲ NOT READ-ONLY: messages are taken from the head and requeued; they come back redelivered=true and their delivery count grows by 1
│ ▲ queue: quorum · ready 1204 · unacked 0 (not visible) · consumers 0
│ ▲ delivery-limit 20 · max seen so far 2/20
╰─ Type the profile name "prod" to approve:
On approval the console reads, masks, caps the output and answers, framed
as untrusted data. The audit log records who asked what and the decision,
never message content.
Every client command, lockmq mcp and lockmq doctor also take --instance NAME, to
reach one lock among several of this type that lea mounts (without it:
$LOCKMQ_INSTANCE, else the one lock in $LEA_LOCKMQ_INSTANCES). lockmq console
and lockmq hub-key take an --instance too, but that one is the name lox up
gives the lock.
Protocols
Protocol
Used for
Profile key
Management HTTP API
every catalogue operation; peek (get with ack_requeue_true)
url (required)
Stream protocol
stream reads, first/last offsets, a named consumer's stored offset
stream_url
AMQP 0-9-1
stream reads (x-stream-offset consumer) when the stream port is not reachable, or through ssh
amqp_url
One broker user for all three. Tested against RabbitMQ 3.13 and 4.1. lockmq
never calls /api/definitions, /api/users*, other users'
/api/permissions*, any non-GET endpoint but the get of peek,
basic.get, basic.publish, a declare, a delete, a purge or store_offset.
Safety model
The console is the enforcement point; skills, rules and tool descriptions
only guide the agent. The full model, its threats and its limits are in
docs/security-model.md. In short:
Separate account. The image holds the console account, the clients
group, the root-owned /etc/lockmq/system.toml and the socket directory
shared through a volume; the console refuses to start without them.
Allowlist. What is not in the catalogue does not exist.
Read-only by default. Tier read reads metadata and streams, and moves
nothing. Tier peek is not strictly read-only and is labelled as such
everywhere.
Human approval. Every read and peek is shown and approved on
lockhub (lox approve, the lea deck or a paired phone); no socket method can approve. On a production profile you
type the profile name. No answer within 5 minutes means denied.
Credentials. Asked at console start (no echo), or read from Vault at
every connection (a container has no keychain). Errors that
leave the console are scrubbed of known secrets, URI userinfo and
Authorization headers.
Broker user. Use a user tagged monitoring with configure "^$" and
write "^$". RabbitMQ lets the read permission purge a queue: grant
read on the streams only, and on queues only for profiles with tier
peek. At start the console refuses an administrator or policymaker
user on production, and asks for a typed continue elsewhere.
Audit. One JSON line per login, policy change, catalogue call,
refusal, decision, run and logout, with the agent's intent and the client's
uid. Never a message body, header or property value.
Peek (tier peek)
The management get takes up to count ready messages from the head and
requeues them in the same call. Consequences, all on the approval screen:
redelivered messages, a delivery count that grows on quorum queues (over the
limit, the message is dead-lettered or dropped), a possible change of order
for active consumers, and only ready messages at the head are visible. The
console reads the effective delivery limit (argument, policy, operator
policy; the lowest; RabbitMQ 4.x's default 20) and refuses a peek within
delivery_headroom of it, from the highest count it saw this session.
Configuration
Profiles live in .lockmq/config.toml at the project root. No file holds a
secret: a URL with a password is refused.
An [profiles.NAME.ssh] table reaches a broker behind a bastion: the console
opens the tunnel itself and checks the bastion's host key; streams are then
read over AMQP.
All profile keys
Key
Default
Notes
url
required
management API, http:// or https://; no userinfo
amqp_url, stream_url
none
optional; an operation that needs a missing one says which key to set
vhosts
every vhost the account sees
the only vhosts served
user
asked at start
with credentials = "vault": the secret's username
credentials
ask
ask, keychain or vault
vault_path
required with vault
e.g. rabbitmq/creds/<role>
credentials_ttl
none
at least 1m: the secret is asked again once it is that old
tls
verify-full
disable, prefer, require, verify-ca, verify-full
tls_ca, tls_cert, tls_key
none
a private CA; a client certificate (both or neither)
tier
read
read or peek
production
false
typed approval, redact-unknown body mode
detectors
card, email, iban, phone
also be_niss, fr_nir, nl_bsn, us_ssn, ip, secret, person_name; [] disables them
Changing the file later: tightenings (a lower limit, peek → read, a
removed vhost, a new mask rule) apply at once; loosenings wait for you in the
console (:review).
Mask rules of the queue read, of the origin queue in a message's first
x-death and of its exchange apply; allow rules only from the queue read, so
a forged x-death can only add masking. The agent may add a mask rule
(lockmq pii add); an allow rule is a loosening the human confirms.
Console keys
Key / command
Does
:review
show the pending loosening and the change requests of clients, apply or refuse
:status
the policy in force, the session
Ctrl-O, :results
print each read and peek result in clear here (the agent still gets it masked)
:unmask
let clients ask for unmasked output (--unmask), each such request approved on lockhub, never auto-approved
:quit, Ctrl-C
end the session
The switches are console-only: no client, config file or environment
variable can set them, and each change is audited.
Limitations
No writes of any kind (purge, move, requeue, publish, shovel), as of v0.6.x.
RabbitMQ only; JSON and UTF-8 text bodies only, other bodies are shown as
size and hash.
Masking is best effort: a value in a free-text field that no detector,
rule or name heuristic catches comes back in clear.
A monitoring user cannot read runtime parameters (shovels, federation)
nor its own permissions: policies says so; permissions are not audited.
Through an ssh tunnel the stream protocol is unavailable (its client dials
the broker's advertised address): streams are read over AMQP, and stream
offsets and consumer lag are not available.
The first peek of a queue cannot know the delivery counts (the count is
only seen after a get); the screen says so.
One console per profile and project, one request at a time.
Roadmap
Not in v0.6.x, and not promised for a given release:
body formats beyond JSON and UTF-8 text.
The lock family
lockmq is one of a family of human-gated tools for AI agents, built on the
same console, separation and approval model:
lockmq runs in Docker. brew trust davidgodefroid/tap then
brew install davidgodefroid/tap/lockmq (or davidgodefroid/tap/lockfamily
for the whole family; Homebrew asks you to trust a third-party tap once) or
scripts/install.sh installs the lockmq launcher,
which runs every command in ghcr.io/davidgodefroid/lockmq:X.Y.Z; the
release's -vault image adds the pinned Vault CLI for
credentials = "vault" (LOCKMQ_VAULT=1). lockmq version prints the
launcher's and the image's versions; lockmq update updates both. See
docs/docker.md, including your own build and migrating
from a host install.
Build
CGO_ENABLED=0 go build ./cmd/lockmq
go test ./...
go test -tags integration ./test/integration/ # RabbitMQ 3.13 and 4.1 in Docker
Requires Go 1.26 or later.
The logo, the demo and the social images are in site/assets, generated by
scripts/brand: python3 scripts/brand/demo.py > site/assets/demo.svg; the
PNGs with render.mjs.
Command lockmq lets an AI agent inspect a RabbitMQ broker through a console the human holds: read-only by default, message content masked, every content read approved.
Command lockmq lets an AI agent inspect a RabbitMQ broker through a console the human holds: read-only by default, message content masked, every content read approved.
Package stream reads the messages of a RabbitMQ stream without moving anything: over the stream protocol with an unnamed consumer (no offset stored, no single-active-consumer group joined), or over AMQP 0-9-1 with an x-stream-offset consumer.
Package stream reads the messages of a RabbitMQ stream without moving anything: over the stream protocol with an unnamed consumer (no offset stored, no single-active-consumer group joined), or over AMQP 0-9-1 with an x-stream-offset consumer.
Package config loads lockmq profiles from the project and user config files, applies the defaults, refuses anything that looks like a secret, and computes the security-relevant policy of a profile.
Package config loads lockmq profiles from the project and user config files, applies the defaults, refuses anything that looks like a secret, and computes the security-relevant policy of a profile.
Package mqconsole is lockmq's console backend for lockkit: its IPC methods (op.plan, op.run), the broker login behind them, the privilege audit of the account, its policy and PII rules.
Package mqconsole is lockmq's console backend for lockkit: its IPC methods (op.plan, op.run), the broker login behind them, the privilege audit of the account, its policy and PII rules.
Package msgmask masks the personal data of RabbitMQ messages: body, headers (application properties) and properties, from the rules of .lockmq/pii.toml, the profile's body mode, field-name heuristics and value detectors (lockkit's pii).
Package msgmask masks the personal data of RabbitMQ messages: body, headers (application properties) and properties, from the rules of .lockmq/pii.toml, the profile's body mode, field-name heuristics and value detectors (lockkit's pii).
Package ops is lockmq's operation catalogue: an allowlist of what a client may ask, each operation with its tier, protocol and whether it needs the human's approval, and the field allowlists that filter what the broker answers before it reaches a client.
Package ops is lockmq's operation catalogue: an allowlist of what a client may ask, each operation with its tier, protocol and whether it needs the human's approval, and the field allowlists that filter what the broker answers before it reaches a client.