lock family docs GitHub

Locks · lockhttp

Using LockHTTP

This page lists every lockhttp command and flag, as the code defines them. For the policy file, see policy.md; for the lea integration, lea.md; for the image, its accounts and volumes, docker.md.

lockhttp <command> [arguments]

lockhttp runs in Docker. On the host, lockhttp is the launcher (scripts/lockhttp): it runs each command in the image, as the account it needs (docker.md).

Command Runs as Volumes
console the console account lockhttp (uid 10021), in the container lockhttp-console, no log driver lockhttp-run, lockhttp-home
profile add, init, doctor you (your uid and gid), from the current directory the project, at its own path
policy lockhttp lockhttp-home
gate gate (uid 10003, in lockhttp-clients), in the container lockhttp-gate, port published on 127.0.0.1:3130 lockhttp-run
signer signer (uid 10004) lockhttp-signer
ca init you (your uid and gid) the --dir directory, at its own path
version, help gate (the image's default) none
update the launcher itself, on the host

lea does not use the launcher for the gate and the signer: it runs them from the same image beside its proxy container (lea.md). A path given to a command is a path inside its container: only ca init and the launcher verbs (the project) mount a host directory; LOCKHTTP_DOCKER_ARGS adds mounts for the others.

Flags use Go's syntax: -flag value, --flag value or --flag=value. Positional arguments are refused.

Exit codes #

Code Meaning
0 ok
1 failed
2 reserved ("no console" in the other Lock family tools; not used)
3 usage or configuration error

Launcher verbs #

lox add sets up every lock of a project through the same three verbs (lockkit's GUIDE.md, "Launcher verbs"):

lockhttp profile add --non-interactive --json --profile NAME [--production]
lockhttp init claude
lockhttp doctor --fix --yes --json

The launcher runs them as you, with the project mounted at its own path and the current directory as the working directory. The project is the nearest directory holding .lockhttp/config.toml, else the nearest holding .git, else the current directory. With --json, a verb prints exactly one line of JSON on stdout, even when it fails: lockkit's verbs.Profile, verbs.Doctor or verbs.Error ({"lock":"lockhttp","code":"usage|secret|exists|failed","error":"..."}); human text goes to stderr.

lockhttp profile add #

lockhttp profile add [--non-interactive] [--json] [--project DIR] [--profile NAME]
                     [--production] [--trusted "a.com .corp.example"]

Adds one profile, with the default policy, to the project's .lockhttp/config.toml, which lox run reviews and copies into the lockhttp console. A new file is written 0600 in a 0700 directory; a profile added to an existing file is appended, the other profiles left byte for byte. A profile already in the file is never rewritten: exit 1, code exists, nothing written.

Flag Default Meaning
--non-interactive off Never read the terminal. Without it, profile add asks on the terminal for what the flags did not give (name, production, trusted hosts), and refuses without a terminal (exit 3).
--json off One line of JSON on stdout: {"lock":"lockhttp","profile":"web","config":"/path/.lockhttp/config.toml","production":false}, or a verbs.Error.
--project DIR the project of the current directory The project (the launcher mounts it).
--profile NAME default Profile name (the table [profiles.NAME]).
--production off A production profile (production = true): its name is typed for every approval, so it needs 2 characters or more and not y, yes, n, no or ok.
--trusted "..." none Hosts to trust for reads (the config key trusted), space or comma separated, in the format of lea's LEA_EGRESS_ALLOW: example.org adds that host; .corp.example adds corp.example and *.corp.example.

lockhttp's target needs no secret: profile add takes no --credentials, and its JSON has no credentials key. No form of it takes, asks for or writes a secret: a flag named like one (--password, --token, ...), a value that embeds a password (user:pw@host, password=) or a positional argument is refused (code secret, or usage for a positional argument) before anything is written, and no error quotes a flag's value.

The profile holds mode = "enforce", heuristics = "shadow", buffer_limit = "10MiB", hold = "60s", connect_grant = "10m", the initial trusted set (Go, Python, npm, Node, MDN, Docker, Kubernetes, man7, docs.anthropic.com, GitHub, crates.io, Debian, Ubuntu, Alpine) plus the --trusted hosts, empty splice and deny lists, and a commented rule example. The file is parsed before it is written.

lockhttp init #

lockhttp init <agent>

<agent> is one of claude, codex, cursor, gemini. lockhttp has no agent files: lea routes the agent's HTTP through the gate. init checks the agent, writes nothing, says so and exits 0. It takes no flag.

lockhttp policy #

lockhttp policy show | edit | path [--config FILE]

The console account's policy file, in its home volume. The subcommand comes first, then the flag.

Subcommand Effect
show Prints the file, then one # profile <name> (managed) comment line per profile of the company's managed configuration.
edit Opens the file in $EDITOR (vi when unset) in the container, then parses every profile in it. A file that does not parse exits 3, and the console keeps the approved policy until it does; otherwise it prints saved: the console shows any loosening for approval (:review).
path Prints the file's path (/home/lockhttp/.config/lockhttp/config.toml in the image).
Flag Default Meaning
--config FILE ~/.config/lockhttp/config.toml The policy file.

show and edit exit 1 when the file does not exist: lockhttp profile add writes the policy in the project, and lox run copies it into the console.

lockhttp console #

lockhttp console [--profile P] [--config FILE]

The policy console: lockkit's console, serving policy.get to the gate. It asks no question about a request: the gate asks lockhub about every held request, and you answer on a facade of the hub (lox approve, lea's deck, a paired phone). It must run in a terminal (exit 3 otherwise), as the console account. Under lox run it runs in its own compose service, with the policy copied from <workspace>/.lockhttp/ (lox attach <instance> attaches its terminal); the launcher runs it in the container lockhttp-console, the only one that mounts lockhttp-home. It is lockkit's policy-only console: it takes no hub flags (lox run passes none to it, only to the gate) and approves no request; one that reached it would be refused. The gate, not the console, needs the hub flags.

Flag Default Meaning
--profile P default Profile to serve. Its socket is user-P.sock in the socket directory.
--config FILE ~/.config/lockhttp/config.toml The policy file.

At the first start of a profile you review its whole policy (Apply these changes? [y/N]); at a later start, a loosening made since the last approval is shown for the same answer. The console reads the policy file again every 2 s. It serves the gate only the policy you approved: a change that loosens it waits until you confirm it with :review (lockkit's "the AI tightens, the human loosens" review).

In the console:

Key or command Effect
:review Review a pending policy change.
:status The console's state.
:quit Stop the console.

Under the company's managed configuration (configuration.md), the console prints Managed by <organisation> (sha256:…) at start and checks the policy against [lockhttp.bounds]: a policy beyond it is refused with one blocked by company policy: … line per key, without a prompt, and the console exits 3; a change beyond it while the console runs is refused at the next poll, never pending. A managed file that cannot be read, is not root's or does not verify is a refusal to start (exit 3).

What an approval on the hub means:

Request Approved
A CONNECT decided at the domain Opens container × host × port for connect_grant, for reads and writes.
A decrypted or plain HTTP request Lets that request through. If the client already left (E_PENDING), the identical request may pass once in the next 10 minutes.

The ticket's screen shows the container (and project), the declared agent and how it was attributed, the method, host and path (the query string as its length only), the body size, Content-Type and fingerprint, the request's header names, lea's task metadata, the rule that decided, warnings in red, the refs of a git push, a masked body preview (first 1 KiB of a textual body, personal data and credentials masked), and the number of distinct requests pending under the same container, method, host and path.

lockhttp managed #

lockhttp managed show
lockhttp managed check [--profile P] [--config FILE]

The company's managed configuration (/etc/lockfamily/managed.toml, see configuration.md), as the console applies it. The launcher runs it as the console account, with its home volume and the host's managed directory read-only on /etc/lockfamily (docker.md).

Subcommand Effect
show Prints the file's path, organisation, fingerprint, signed (yes or no) and lockhttp's bounds, defaults and profiles, managed rules included.
check Loads the console's policy file as the console does and checks every profile, or the one named, against the bounds and the managed rules. It opens no connection.
Flag Default Meaning
--profile P every profile check: the profile to check.
--config FILE ~/.config/lockhttp/config.toml check: the policy file.

Output and exit codes are a contract (lox runs managed check before it starts the lock):

Case show check
No managed file no managed configuration, exit 0 no managed configuration, exit 0
Within the bounds the file, exit 0 profile P: within the company's managed configuration per profile, exit 0
Beyond the bounds one blocked by company policy: <key> … (profile P: …) line per violation on stdout, exit 3
A refused file the lockkit error on stderr, exit 3 blocked by company policy: <the lockkit error>, exit 3

lockhttp doctor #

lockhttp doctor [--fix [--yes]] [--json] [--profile P] [--ca-cert FILE] [--gate ADDR]

A launcher verb: checks the setup and the project of the current directory and prints one line per check (✓, ▲ warning, ✗ failure). Exit 0 exactly when no check fails (a warning does not fail), 1 otherwise.

Flag Default Meaning
--fix off Repair what can be (the write permission others have on the project's policy or its .lockhttp directory), asking before each repair, then check again. Without a terminal it needs --yes (exit 3 otherwise).
--yes off With --fix: repair without asking. Alone, a usage error (exit 3).
--json off One line of JSON on stdout: {"lock":"lockhttp","ok":true,"fixed":[],"checks":[{"check":"policy","status":"ok","detail":"...","fix":"..."}]}; a usage error prints a verbs.Error instead (the code key rather than checks).
--profile P every profile Profile to check in the project's policy.
--ca-cert FILE none CA certificate to check (fails under 7 days of validity). Without it, the doctor warns that decryption is off.
--gate ADDR none Gate address to dial (for example 127.0.0.1:3130).

The checks: setup, the separated setup (the image's /etc/lockhttp/system.toml, and allowed_uids when set); agent, nothing to wire; managed, the company's managed configuration (Managed by … (sha256:…), no managed configuration, or a failure with the lockkit error); policy, the project's .lockhttp/config.toml (missing: run lockhttp profile add), each profile parsed as the console loads it, with its mode, heuristics and counts, a warning in shadow mode, a failure with the blocked by company policy: … lines when it goes beyond the managed bounds; permissions, the policy and its directory writable by their owner only (--fix removes the others' write permission); ca and gate.

It runs in a container of its own: --ca-cert names a file inside it (a file in the project is at its own path), and 127.0.0.1 is that container. Mount the CA and join the gate's network namespace with LOCKHTTP_DOCKER_ARGS, for example LOCKHTTP_DOCKER_ARGS="--network container:lockhttp-gate -v $PWD/ca.pem:/tmp/ca.pem:ro" lockhttp doctor --gate 127.0.0.1:3130 --ca-cert /tmp/ca.pem.

lockhttp ca init #

lockhttp ca init --dir DIR [--name NAME] [--force]

Creates the local CA for decryption. The launcher creates DIR on the host (mkdir -p), mounts it at its own path and runs the command as you, so the files are yours.

Flag Default Meaning
--dir DIR required Directory (created 0700) for ca.pem (0644) and ca-key.pem (0600).
--name NAME the host name Installation name in the CA's subject. Through the launcher the host name is the container's: pass --name.
--force off Replace an existing CA (rotation). Without it, an existing ca-key.pem is refused.

The CA is ECDSA P-256, pathlen:0, valid 90 days. Trust ca.pem in the agents' containers only; give ca-key.pem to the signer only.

lockhttp gate #

lockhttp gate --listen ADDR --hub URL --hub-key-file FILE --instance NAME
              [--home DIR] [--hub-net CIDR ...] [--hub-plain-http]
              [--devices-file FILE [--require-device RISKS] [--seal-clear]]
              [--facade-keys-file FILE]
              [--upstream URL] [--profile P] [--sources FILE]
              [--credentials FILE] [--secrets FILE] [--audit-dir DIR] [--status-file FILE]
              [--ca-cert FILE --signer SOCKET]
lockhttp gate --check [--listen ADDR]

The HTTP(S) proxy. It runs hub-attached only: without --hub, --hub-key-file and --instance it exits with a usage error (exit 3, "launched by lox run only"); lea passes them from lox run's up.json. It refuses to run as root; in the image it runs as gate (uid 10003). Each flag but --check has an environment variable, read when the flag is not given.

Through the launcher, lockhttp gate runs in the container lockhttp-gate with the socket volume, publishes its port on LOCKHTTP_PUBLISH (127.0.0.1:3130 by default), and listens on 0.0.0.0:3130 unless you pass --listen. lea runs its own gate (lea.md).

Flag Environment Default Meaning
--listen ADDR LOCKHTTP_LISTEN 127.0.0.1:3130 Address to listen on.
--upstream URL LOCKHTTP_UPSTREAM none Proxy to send through, http://host:port (lea: squid, http://127.0.0.1:3128). Empty: the gate dials targets itself and refuses non-public addresses (except internal hosts).
--profile P LOCKHTTP_PROFILE default Console profile to dial.
--sources FILE LOCKHTTP_SOURCES none sources.json written by lea. Empty: every connection is container local. An unreadable or invalid file at start is a configuration error; later, it refuses every connection until fixed.
--credentials FILE LOCKHTTP_CREDENTIALS none credentials.json written by lea: hashes of each container's Claude credentials. Empty: the check is off. Unreadable or invalid: every credential is unknown (denied).
--secrets FILE LOCKHTTP_SECRETS none The gate's secrets file: the values the policy's inject entries name (policy.md). JSON {"version": 1, "secrets": {"name": "value"}}, mode 0600 (refused if group- or world-readable), in the gate's container only, never an agent's. Reloaded when it changes. Invalid at start: a configuration error; later, every injection is refused (E_INJECT).
--audit-dir DIR LOCKHTTP_AUDIT_DIR none (stderr) Directory (created 0700) of the JSONL audit: gate-YYYY-MM-DD.jsonl (UTC), files 0600, kept 30 days.
--status-file FILE LOCKHTTP_STATUS_FILE none Status file for lea, rewritten atomically on change: version, console (up/down), pending, pending_by_container, policy_fp, updated.
--ca-cert FILE LOCKHTTP_CA_CERT none CA certificate. Enables decryption, with --signer.
--signer SOCKET LOCKHTTP_SIGNER none The signer's Unix socket. --ca-cert and --signer go together.
--hub URL LOCKHTTP_HUB none (required) The lockhub URL the gate asks. --hub, --hub-key-file and --instance are required together.
--hub-key-file FILE LOCKHTTP_HUB_KEY_FILE none (required) The pinned public key of the hub (Ed25519, base64 on one line). Decisions verify under it only.
--instance NAME LOCKHTTP_INSTANCE none (required) The lock instance name the hub knows the gate by.
--home DIR LOCKHTTP_HOME the home directory The gate's own volume (0700, the gate's uid only): the instance key (hub/NAME.key, made by lockhttp hub-key), the plans and the hub state (hub/NAME/). Never mount it in another container.
--hub-plain-http LOCKHTTP_HUB_PLAIN_HTTP=1 off Allow an http:// --hub URL to a hub on a network of its own: a service name without a dot (lockhub) or a private address only. Without it an http:// hub is refused unless it is a loopback address; use https. Requests and decisions are signed either way.
--facade-keys-file FILE LOCKHTTP_FACADE_KEYS_FILE none The facade keys the gate pins (JSON, written by lox run). A reveal (Ctrl-O on a local facade of the hub) is answered with the ticket's clear screen sealed to the facade's ephemeral key, only when one of these keys signed it; the hub relays ciphertext. Without it, reveals are refused.
--devices-file FILE LOCKHTTP_DEVICES_FILE none The approval devices the gate pins (JSON, written by lox run at /run/lockfamily/devices.json). A decision taken on a device (the hub's mobile facade) is acted on only if it carries the signature of one of them over the gate's own signed ticket. Without it, every decision of the mobile facade is refused.
--require-device RISKS LOCKHTTP_REQUIRE_DEVICE none Risk classes, comma-separated (write, prod-write), whose approvals must be signed by a pinned device, from any facade: an approval without one is a denial. The gate's asks are write, or prod-write under a production policy. Needs --devices-file.
--seal-clear LOCKHTTP_SEAL_CLEAR=1 off Send each ticket's clear screen (the console's, personal data unmasked) sealed to every pinned device; the hub relays ciphertext. Off, the clear screen never leaves the gate. Needs --devices-file.
--hub-net CIDR none A network of the hub. The gate refuses as a target the hub's host name, its addresses (resolved at start) and any name resolving into these networks, even when internal_hosts allows private addresses; a name that does not resolve is refused too. Repeatable. Without it, the hub host must resolve at start, or the gate does not start.
--check off Exit 0 if something answers on --listen (3 s), else 1. Starts nothing.

Asking the hub. An ask becomes a plan in the gate's volume and a ticket signed with the instance key, pushed to the hub with the screen masked (personal data and credentials). The request answers E_PENDING after hold. A decision that verifies under the pinned hub key, for this instance and this request's fingerprint, under the policy in force, grants the single-use authorisation (10 minutes) or, for a CONNECT, the connect grant; its nonce is consumed once. A denial answers E_DENIED to a waiting request. The risk class is write (any part of a held request can carry data the agent chose), prod-write under a production policy. The console serves the policy; it is never asked about a request. Hub decisions are audited once (approved/denied, approval hub, ticket, approver, facade, hub_sig); the request run after an approval within the hold is audited approved with approval hub-run, not as a second decision. An unreachable hub leaves the ask pending: nothing falls back to another approver.

The E_PENDING answer names the ticket (X-LockHTTP-Ticket and a hub ticket: line in the body); a retry of the same request waits on the same ticket (when the hub is slow to take the ticket, past hold plus one second, the E_PENDING may come without one). A ticket that expires undecided (24 h for write, 4 h for prod-write) is asked again rather than refused: while the agent keeps sending the request, the gate renews it at once; otherwise the ask ends (E_TIMEOUT, audited timeout, naming the ticket; its hint says to send the same request again) and the same request sent later (within 24 h, same gate process) renews it. A renewal the human made on the hub is picked up by the request sent again; once it ran, was denied or expired too, the request sent again is a new ticket. A renewal is a new ticket that names the expired one (renews), re-checked against the policy in force (a changed policy is a new ticket instead); the agent's retries get E_PENDING with the new ticket, and an approval lets the request through once. A plan of the gate is the request itself, so a renewal never shows a moved input. A human can also ask again from the hub. The plan hash inputs the gate binds are request (the request fingerprint) and policy.

The gate keeps each ticket's clear screen (the console's screen, the body preview masked as there) in its volume; it leaves only sealed: to a pinned facade key (--facade-keys-file) on a reveal, and with --seal-clear to each pinned device. Reveals and renewals asked by the hub are audited (reveal/renew, approval hub-claimed).

Supported lockhub versions. Which lockhub releases each lockhttp release works with. Under lox run, lea runs the gate in hub mode (lockhub's docs/lox-run.md), and only from an up.json that says what the gate pins ("lockhttp_gate": 1): that needs lockhub v0.2.2 or later and lea v0.3.0 or later, for every lockhttp release below. A gate run by hand works with the hub image of the last column. From v0.6.0 the gate is hub-attached only (there is no standalone gate): run it under lox run, with a trusted up.json naming one lockhttp lock. lea after v0.3.5 (lea#232) then refuses to start a gate without one and says to run lox run; lea v0.3.0 to v0.3.5 instead falls back to a standalone gate, which v0.6.0 and later refuse: that gate container exits and restarts (nothing gets out).

lockhttp lockkit Under lox run (lea's gate) Hub image, gate run by hand
v0.8.1 v0.14.1 as v0.6.0 v0.2.0 or later
v0.8.0 v0.13.0 as v0.6.0 v0.2.0 or later
v0.7.1 v0.10.0 as v0.6.0 v0.2.0 or later
v0.7.0 v0.10.0 as v0.6.0 v0.2.0 or later
v0.6.0 v0.9.1 lockhub v0.2.2 + lea v0.3.0 or later, with a trusted up.json only: hub mode only (no standalone gate), reveals (reveal: true), renewals, devices: true v0.2.0 or later
v0.5.1 v0.8.1 lockhub v0.2.2 + lea v0.3.0 or later: hub mode, reveals (reveal: true), renewals, devices: true v0.2.0 or later
v0.5.0 v0.6.0 lockhub v0.2.2 + lea v0.3.0 or later: hub mode, reveals (reveal: true), renewals, devices: true v0.2.0 or later
v0.4.0 v0.4.0 lockhub v0.2.2 + lea v0.3.0 or later: hub mode, devices: true; no reveals, no renewals v0.1.1 or later; device-signed decisions from v0.2.0 (its mobile facade)
v0.3.0 v0.3.1 lockhub v0.2.2 + lea v0.3.0 or later: hub mode only v0.1.1 or later
v0.2.1 and older older than v0.3.1 none (no hub mode) none

lea checks the pinned lockhttp release and never passes the gate a flag it does not know: with reveal: true on a gate older than v0.5.0 it warns and pins no facade key (no reveal); with devices: true on one older than v0.4.0 it warns and pins no device, or refuses to start the gate when require_device or seal_clear is set. A gate on lockkit older than v0.5.0 ignores the hub's reveal and renewal requests, which then time out. lockhub's CHANGELOG ("Supported locks") and lea's internal/launcher/lockhubgate.go are the references.

The gate finds the console's socket as a lockkit client, in the socket directory of the image's /etc/lockhttp/system.toml: /run/lockhttp, the console's socket volume (lox run's <project>_<instance>-run) it shares with the console. It polls the policy every 2 s and reloads --sources, --credentials and --secrets when they change.

Clients reach the gate as an HTTP proxy: CONNECT host:port for HTTPS, absolute http:// URLs for plain HTTP. Proxy-Authorization: Basic carries the agent name and token (the token is matched in sources.json; without one the name is recorded as given).

On stderr the gate logs its start (listening on ..., profile ..., domain (CONNECT + SNI) or decrypting (signer ...)), the console going up or down, and policy changes (by fingerprint).

lockhttp signer #

lockhttp signer --ca-cert FILE --ca-key FILE --listen SOCKET [--allow-uid N ...]

The only process that reads the CA key. It issues 24-hour leaf certificates (capped at the CA's expiry) to the gate over a Unix socket. In the image it runs as signer (uid 10004). The launcher mounts nothing for it: lea runs it with the key and a socket directory shared with the gate (lea.md).

Flag Default Meaning
--ca-cert FILE required CA certificate.
--ca-key FILE required CA key. Refused if readable by group or others (chmod 0600).
--listen SOCKET required Unix socket path (replaced if present, mode 0660).
--allow-uid N the signer's own uid uid allowed to ask for leaves, checked with the kernel's peer credentials. Repeatable.

It warns when the CA expires within 7 days. It issues leaves only for a valid host name (no wildcard, no IP literal).

lockhttp hub-key #

lockhttp hub-key --instance NAME [--home DIR]

Prints the gate's instance public key (Ed25519, base64 on one line) for lox run to pin at the hub. The first call makes the key in DIR/hub/NAME.key (0600); --home defaults to LOCKHTTP_HOME, then the home directory.

lockhttp version, lockhttp help #

lockhttp version (or --version) prints the launcher's version and the image it runs (lockhttp launcher X (image I)), then the image's lockhttp vX; lockhttp help (-h, --help) prints the usage. lockhttp without a command prints the usage on stderr and exits 3.

lockhttp update #

lockhttp update [vX.Y.Z]

A launcher command, run on the host: replaces the launcher with a release's (latest by default) after checking the cosign signature of checksums.txt, the archive's checksum and the image's signature, then docker pulls the pinned image (docker.md).

Files #

File Where Owner Content
/usr/local/bin/lockhttp host root the launcher
/usr/local/bin/lockhttp image root the binary, the image's entrypoint
/etc/lockhttp/system.toml image root, 0644 separated setup: console account, clients group, socket directory, allowed_uids (build arg ALLOWED_UIDS)
/run/lockhttp/ volume lockhttp-run lockhttp:lockhttp-clients, 2710 the console's sockets, user-<profile>.sock
~/.config/lockhttp/config.toml volume lockhttp-home lockhttp, 0600 the policy
~/.local/state/lockhttp/audit.log volume lockhttp-home lockhttp the console's audit (approvals, policy reviews)
--audit-dir/gate-YYYY-MM-DD.jsonl the gate's container (lea: a host directory) the gate's uid the gate's audit
ca.pem, ca-key.pem the ca init --dir directory you; lea gives the key to the signer's uid the local CA

Audit records #

Both audits write one JSON object per line with lockkit's base fields (event, decision, code, reason, warnings, approval, profile, ...) and lockhttp's: request_id, container, project, via, agent, attribution, role, parent, task_id, branch, session_id, phase, tls (connect, mitm, plain), method, host, port, path, query_len, query_hash, req_headers (names only), req_bytes, body_fp, rule, class, score, shadow_score, shadow, status, resp_bytes, duration_ms, policy_fp. Never a body, a query string or a header value.

decision is free, approved, refused, or shadow-ask / shadow-deny for what shadow mode let through.