How lockmq works
Two processes in two accounts. Only the console holds the key.
Two processes
The console (lockmq console) runs in a session of the dedicated lockmq account. It holds the broker credentials and the only broker connections, applies the policy, masks messages and asks you to approve every read of message content.
The clients (the lockmq CLI and lockmq mcp, the MCP server on stdio) run on the agent’s side. They hold no secret and open no broker connection: they talk to the console over a Unix socket that only the lockmq-clients group reaches, and the kernel’s peer credentials are checked on every connection.
agent account lockmq account (its own session)
┌──────────────────────┐ socket ┌─────────────────────────────────────┐
│ Claude Code / Codex │ ──────────▶ │ lockmq console │
│ lockmq mcp / CLI │ ◀────────── │ policy · allowlist · masking │
│ (no secret, no │ JSON-RPC, │ approval screens · audit log │
│ broker connection) │ peer uid │ credentials (prompt/keychain/Vault)│
└──────────────────────┘ checked └───────────────┬─────────────────────┘
│ management API (GET)
│ stream protocol / AMQP
▼
RabbitMQ
The path of a request
- The agent calls an MCP tool (
lockmq_queues,lockmq_read, …) or the CLI. - The console checks that the operation exists, its protocol is configured, the vhost is served by the profile, the tier allows it and the arguments are in range, before any broker call.
- Catalogue operations run at once and are audited.
readandpeekbuild an approval screen and wait for you. - The console calls the broker, keeps only the fields lockmq renders, masks, caps the output and audits the call (never message content).
- The answer goes back framed as untrusted data.
The catalogue: an allowlist
What is not listed does not exist. Every operation is a CLI command and an MCP tool.
| Command | Returns | Approval |
|---|---|---|
lockmq overview | broker version, nodes and their alarms, feature flags, totals | none |
lockmq queues | one page of queues, filtered (--filter GLOB) and sorted by name, messages, ready, unacked or consumers | none |
lockmq queue NAME | one queue with its arguments, effective policy, bindings and consumers | none |
lockmq exchanges, bindings | exchanges; bindings of a vhost, a queue or an exchange | none |
lockmq policies | policies, operator policies, runtime parameters (URI passwords removed) | none |
lockmq consumers, connections, channels | with prefetch and ack mode; on the served vhosts only | none |
lockmq stream NAME | a stream’s queue data and first/last offsets | none |
lockmq stream-offset NAME --consumer REF | a named consumer’s stored offset and its lag | none |
lockmq read STREAM --from first|last|N|TIMESTAMP | stream messages, masked | you, every time (auto mode outside production) |
lockmq peek QUEUE --count N | the head of a classic or quorum queue, masked; tier peek only | you, every time, never auto |
--dry-run on read and peek asks the console for its verdict and the approval screen without asking you anything and without reading a message. All client commands.
Three protocols, one broker user
| Protocol | Used for | Profile key |
|---|---|---|
| Management HTTP API | every catalogue operation; peek | url (required) |
| Stream protocol | stream reads, first/last offsets, a named consumer’s stored offset | stream_url |
| AMQP 0-9-1 | stream reads when the stream port is not reachable, or through ssh | amqp_url |
An operation that needs a protocol you did not configure is refused with the key to set. Tested against RabbitMQ 3.13 and 4.1.
Stream reads stay read-only
Over the stream protocol the consumer is opened without a name: no offset is stored and no single-active-consumer group is joined; it reads at most read_max messages, then closes. Over AMQP 0-9-1 it is a consumer with x-stream-offset and manual acks (on a stream an ack only returns credit; nothing is removed). A real consumer’s position never moves: store_offset is never called.
Queue peek: a per-profile setting
Reading a classic or quorum queue is not strictly read-only. The management get takes up to count ready messages from the head and requeues them in the same call: they come back redelivered=true, a quorum queue’s delivery count grows by one (over the limit, the message is dead-lettered or dropped), the order seen by active consumers can change, and only ready messages at the head are visible.
So peek exists only on a profile you set to tier = "peek" (lockmq setup asks, default no). Every peek is approved by you, never by auto mode. On a quorum queue 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.
╭─ 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:
Masking messages
Masking covers message properties, application properties, headers and bodies. Queue, exchange and stream names are kept.
- JSON bodies are parsed and masked per leaf.
body_mode = "redact-unknown"(the production default) redacts every string no allow rule names;detectuses mask rules, key-name heuristics and value detectors (email, phone, IBAN, card; national ids opt-in). - Other bodies: UTF-8 text goes through the detectors; binary, compressed or invalid bodies become
<bytes:N sha256:H type:T>. - References: a redacted value becomes
<redacted:rN.K>, the same reference for the same value within a result; the clear value stays in the console’s memory. - Dead-letter queues: mask rules of the queue read, of the origin queue in a message’s first
x-deathand of its exchange apply; allow rules only from the queue read, so a forged header can only add masking.
# .lockmq/pii.toml
[[mask]]
queue = ["orders/order-events"] # vhost/name globs; "//name" for the vhost /
body = "$.customer.email" # JSON path: $.a.b, $.a[*].b, $.a[3]
mode = "email" # redact (default) | partial | email
[[mask]]
queue = ["*/*"]
header = "x-customer-*"
[[allow]]
queue = ["orders/order-events", "orders/order-events.dlq"]
body = "$.items[*].sku"
The AI tightens, the human loosens
The console reads the config files every 2 seconds. A tightening (a lower limit, peek → read, a removed vhost, a new mask rule) applies at once, whoever wrote it. A loosening (read → peek, a larger limit, a lower headroom, a new vhost or URL, redact-unknown → detect, a new allow rule) waits for you: :review in the console shows it, and you apply or refuse it. No socket method loosens the policy. The agent may add mask rules (lockmq pii add) and queue change requests (lockmq request).
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 |
Shift+Tab, :mode | auto mode on/off (never on production; never for peek) |
:quit, Ctrl-C | end the session |
The switches are console-only and each change is audited. The session ends after idle_timeout without a request, after max_session, or on lockmq logout.
Refusals the agent can act on
| Code | Retry | Means |
|---|---|---|
E_BAD_ARGS | rewrite | an argument is missing, out of range or unknown |
E_WRONG_QUEUE_TYPE | rewrite | read on a queue or peek on a stream |
E_VHOST_NOT_SERVED | ask the user | the profile does not serve that vhost |
E_PROTOCOL_NOT_CONFIGURED | ask the user | set stream_url or amqp_url |
E_TIER | ask the user | peek on a profile with tier read |
E_DELIVERY_LIMIT | ask the user | a peek could dead-letter or drop messages of this quorum queue |
E_POLICY | stop | anything else the policy refuses |
MCP clients get them as tool errors with a structured refusal (code, retry, hint); CLI exit codes are 0 ok, 1 refused, denied or failed, 2 no console, 3 usage or configuration error.