18 KiB
Journey 13 — Security & Governance
You're putting lean-ctx in front of real code — possibly on a shared machine, in CI, or under a security policy. This journey covers every guardrail lean-ctx applies by default and every dial you can tighten: filesystem jail, shell allowlist, secret redaction, OS sandboxing, harden mode, and role policies.
Source files:
rust/src/core/pathjail.rs— filesystem boundary (PathJail)rust/src/core/shell_allowlist.rs— command allowlistrust/src/core/secret_detection.rs— secret scanning & redactionrust/src/core/sandbox.rs,sandbox_seatbelt.rs,sandbox_landlock.rs— OS sandboxrust/src/core/tcc_guard_sandbox.rs— macOS launchd seatbelt wrapper (deny~/Documents, #356)rust/src/cli/harden.rs—harden(soft/hard/undo)rust/src/core/context_policies.rs— role policiesrust/src/core/owasp_alignment.rs,audit_trail.rs— alignment & audit
0. The defense-in-depth model
Every file read and shell command flows through layered guardrails, on by default — you don't opt in:
flowchart LR
R[ctx_read / ctx_shell] --> PJ[PathJail<br/>stay in project root]
PJ --> AL[Shell allowlist<br/>~200 known binaries]
AL --> SB[OS sandbox<br/>Seatbelt / Landlock]
SB --> SD[Secret redaction<br/>before output leaves]
SD --> OUT[result to the agent]
You can tighten each layer (stricter shell parsing, harden mode, role policies) but you cannot accidentally turn off the baseline.
0.1 See and flip the whole posture — lean-ctx security
lean-ctx's guardrails fall into two independent planes. Keeping them separate is deliberate: a usability-first user can let the agent do anything on a trusted machine without ever leaking secrets to the model provider.
| Plane | Protects | Controls |
|---|---|---|
| Containment | your machine from the agent | PathJail + shell gating |
| Secret defense | your secrets from the LLM provider | .env / credential redaction |
One command shows the live posture and how to change it:
lean-ctx security status # posture board: jail, shell, secret redaction
Two master switches flip containment in one step — secret redaction is never touched by them:
lean-ctx yolo # OPEN: any path + any command (containment off)
lean-ctx secure # STRICT: restore the secure defaults
yolo writes path_jail = false and shell_security = "off" to your global
config and takes effect immediately (no restart). It is fully reversible, and you
re-enable pieces granularly afterwards — the array/blanket dichotomy works in
both directions:
lean-ctx config set shell_security warn # log violations, block nothing
lean-ctx config set path_jail true # re-enable the filesystem jail
lean-ctx allow acli # permit one extra command (additive)
Secret/.env redaction is a separate toggle — yolo leaves it on:
lean-ctx security secrets off # stop masking secrets (NOT recommended)
lean-ctx security secrets on # re-enable masking
lean-ctx security status (and lean-ctx doctor) print a coarse label —
STRICT (both planes enforced), RELAXED (partially loosened), or OPEN
(containment fully off) — so the active posture can never hide.
Mental model: containment is "what the agent may touch on this machine"; secret defense is "what may leave this machine for the provider".
yoloonly drops the first.
1. PathJail — stay inside the project
What it does: confines file access to the resolved project root. Absolute
paths outside the jail are rejected, so a stray read /etc/passwd or a
path-traversal ../../ cannot escape the workspace.
- Re-rooting to a different project root is off by default
(
allow_auto_reroot = false) — lean-ctx will not silently follow an absolute path into another tree. - Multi-root setups (
lean-ctx serve --root a:A --root b:B) jail each root independently (server/multi_path.rs). - Widen it (array): add trusted roots with
allow_paths/extra_roots(orLEAN_CTX_ALLOW_PATH) — reads/writes resolve under those prefixes too. - Disable it (blanket): set
path_jail = falseto allow any path — the blanket equivalent ofallow_paths = ["/"], for containers/sandboxes where the boundary is already external.lean-ctx yolosets this for you, andlean-ctx doctorflags it loudly while it is active.
This is the foundation every other layer assumes.
1.5 Workspace trust — gate untrusted project-local config
What it does: a cloned repo ships its own .lean-ctx.toml, which is merged
over your global config. That merge can raise security-sensitive settings —
replace the shell_allowlist, widen the jail via allow_paths / extra_roots,
repoint a proxy.*_upstream, define custom_aliases, or flip
rules_scope / rules_injection. lean-ctx treats those as trusted-only.
For a workspace you have not trusted, the sensitive overrides are withheld
(comfort knobs like compression_level / theme still apply) and a [SECURITY]
warning is logged. Grant trust explicitly:
lean-ctx trust # trust the current project root
lean-ctx trust status # show trust state + which overrides are gated
lean-ctx trust --list # list all trusted workspaces
lean-ctx untrust # revoke trust for the current root
Trust is pinned to both the workspace path and a content hash of
.lean-ctx.toml — editing the file after trust re-gates it, so a "trust once,
modify later" change can't take effect silently. lean-ctx doctor shows the
state. Headless / fleet environments can opt in without a prompt via
LEAN_CTX_TRUST_WORKSPACE=1 or a LEAN_CTX_TRUSTED_ROOTS=/path/a,/path/b list.
Review a clone's
.lean-ctx.tomlbefore youlean-ctx trustit — trusting is what lets its security-sensitive settings widen lean-ctx's own boundaries.
2. Shell allowlist & strict mode
What it does: the shell hook only compresses/executes commands whose binary
is on the allowlist (~200 common dev tools: git, cargo, npm, node,
python, …). Anything else passes through untouched rather than being wrapped.
Pipelines & chains (|, &&, ||, ;): a compound command is wrapped as a
single lean-ctx -c "<whole>" only when every stage is gate-clean — then
the pipe/chain runs natively inside one profile-free shell and just the final
output is compressed (so git log | wc -l counts the raw log, not a digest). If
any stage is not gate-clean (an interpreter -c, a non-allowlisted sink such as
kubectl, …) the whole command is left raw for the agent's shell. lean-ctx
never wraps only part of a pipe (which would compress mid-pipe data the next stage
still needs) and never relaxes the gate to wrap a tricky sink — the rewrite can
only ever route commands the same chokepoint would already allow.
# config.toml
shell_allowlist = ["git", "cargo", "npm", "…"] # REPLACE the default set
shell_allowlist_extra = ["just", "task"] # ADD to the default set
shell_strict_mode = false # set true to block $() and backticks
excluded_commands = [] # never intercept these
docker/podmanare deliberately not in the default allowlist — mount flags can bypass PathJail. Add them explicitly only if you accept that.shell_strict_mode = trueblocks command substitution ($(…), backticks) for environments that must forbid dynamic command construction.- Replace the whole list via
LEAN_CTX_SHELL_ALLOWLIST(comma-separated), or just add a few extras withshell_allowlist_extra. Thelean-ctx allow <cmd>CLI editsshell_allowlist_extrafor you, andlean-ctx allow --listprints the effective allowlist plus any parse errors so a typo can never silently drop your overrides.
2.1 Shell-security mode (enforce | warn | off)
One switch governs all command gating — the allowlist and the hard blocks
(eval/exec/source, $()/backticks at command position, interpreter -c).
It is applied at a single chokepoint, so MCP ctx_shell and the CLI
(lean-ctx -c / -t) behave identically.
# config.toml
shell_security = "enforce" # default — secure
# shell_security = "warn" # run the checks, log violations via tracing, never block
# shell_security = "off" # skip command gating entirely — compression stays active
| Mode | Allowlist | Hard blocks (eval, $(), …) |
Compression |
|---|---|---|---|
enforce (default) |
enforced | blocked | on |
warn |
logged only | logged only | on |
off |
skipped | skipped | on |
- Resolution:
LEAN_CTX_SHELL_SECURITYenv →shell_securityin config → defaultenforce. An unknown value falls back toenforce(never fails open). - Why
enforceis the default, even for beginners: lean-ctx mediates the agent's shell, so a weaker default would silently downgrade security for every install on upgrade. In practice the default allowlist already covers normal workflows (git/cargo/npm/node/python/…), so beginners rarely hit it — the friction is for power users running exotic tools, who opt intooff. offis the "YOLO" escape hatch. It disables command gating only; it does not lift the read-only-output doctrine inctx_shell(no>/tee/heredoc file writes — use the native write tool). Compression is unaffected in every mode. Set it together withpath_jail = falsein one step vialean-ctx yolo(and revert both withlean-ctx secure).lean-ctx doctorsurfaces the active mode whenever it is notenforce, so a relaxed posture can never hide.- This supersedes the CLI-only
LEAN_CTX_ALLOWLIST_WARN_ONLY(which still works for backward compatibility);shell_securityis the canonical, global switch.
3. OS sandboxing for executed code
What it does: when lean-ctx executes code (ctx_execute), it runs under the
OS sandbox — Seatbelt on macOS and Landlock on Linux — so the executed
process gets a restricted filesystem/network view, not your full user
privileges. On platforms without a supported sandbox, execution is gated rather
than run unconfined.
This is separate from PathJail (which guards lean-ctx's own reads); the sandbox guards child processes lean-ctx spawns on your behalf.
3.1 launchd seatbelt — no macOS "Documents" prompt (#356)
On macOS the daemon, proxy and auto-updater run as LaunchAgents — i.e. as their
own TCC identity (ppid 1), which does not inherit your terminal's or
editor's privacy grants. Any access they make under ~/Documents, ~/Desktop
or ~/Downloads would pop the "lean-ctx would like to access files in your
Documents folder" prompt, and because every release re-signs the binary a
granted permission is voided on the next update — so the prompt returns forever.
lean-ctx therefore bakes a deny-~/Documents Seatbelt profile into the
LaunchAgent invocation itself (rust/src/core/tcc_guard_sandbox.rs): the plist
runs sandbox-exec -p '(allow default) (deny file-read* file-write* …)' lean-ctx …. The kernel refuses any access to the three protected directories silently
with EPERM — the TCC subsystem is never consulted, so the prompt can never
appear and no "Allow" is ever required. Everything outside those directories
stays permitted, so the processes keep full functionality. The path guards
(pathutil::may_probe_path) and the optional stable code-signing identity
(lean-ctx codesign-setup) remain underneath as defense-in-depth. Verified by
rust/tests/tcc_sandbox.sh, which boots the daemon under the production wrapper.
4. Secret redaction — nothing leaks to the model
What it does: before any shell output or file content is returned, lean-ctx
scans it for credentials (AWS keys, tokens, etc.) and replaces matches with
[REDACTED:<kind>]. It is on by default.
[secret_detection]
enabled = true
redact = true
custom_patterns = ["MYCORP_[A-Z0-9]{20}"] # add org-specific secret shapes
exclude_patterns = ["LCTX_PUBLIC_\\w+"] # carve out known-safe matches (subtractive)
Built-in patterns cover common cloud/credential formats; custom_patterns lets
you redact organization-specific secret shapes. Matches are reported with a safe
preview (e.g. AKIA…) so you know redaction fired without seeing the secret.
exclude_patterns is the subtractive counterpart (#718): a detected match
covered by any of these regexes is neither reported nor redacted — carve out a
repo's own naming conventions without disabling secret detection wholesale.
Since v3.9.2 the detector also skips benign non-values on its own: unquoted
identifier/property references (serverEnv.getStripeSecretKey), camelCase
subwords (superuserPassword: as a key), and obvious placeholders
(ghp_change_me, your_key_here). Quoted string literals and digit-bearing
values remain protected.
4.1 Sensitivity policy floor — per-item levels
Where [secret_detection] masks known credential shapes, the sensitivity
floor classifies every item by a level and enforces one uniform policy
floor just before content reaches the model. It is off by default (fully
no-op) and covers both tool outputs and injected knowledge facts.
[sensitivity]
enabled = true # off by default → no-op
policy_floor = "secret" # public < internal < confidential < secret
action = "redact" # "redact" masks the spans, "drop" withholds the item
| Level | Raised by (high-precision signals only) |
|---|---|
secret |
secret-like paths (.env, .ssh/…, *.pem) or detected credentials |
confidential |
Luhn-validated card numbers, mod-97-validated IBANs |
internal |
reserved for explicit tagging |
public |
default |
Anything classified at or above policy_floor is dropped or redacted: with
action = "drop" the whole item is replaced by a short notice; with redact
only the offending spans are masked. Knowledge facts store their level at
creation (KnowledgeFact.sensitivity) and are re-checked at injection time, so a
floor change takes effect immediately. The classifier uses only high-precision
signals (no speculative heuristics) to avoid false positives; LEAN_CTX_SENSITIVITY=0|1
toggles enforcement for a single run. This section lives in the global
~/.lean-ctx/config.toml only — an untrusted project file cannot lower the floor.
5. Harden mode — force the compressed path
What it does: harden makes the agent use lean-ctx's compressed ctx_*
tools by denying native Read/Grep (except immediately after an Edit). Two
levels:
lean-ctx harden # soft: sets LEAN_CTX_HARDEN=1 in MCP configs
lean-ctx harden --hard # also adds "Bash" to ~/.claude/settings.json deny
lean-ctx harden --undo # remove both — native tools allowed again
Real output:
lean-ctx harden (level: soft)
[OK] /Users/you/.cursor/mcp.json
[OK] /Users/you/.claude.json
[OK] Set LEAN_CTX_HARDEN=1 in MCP configs
Harden active. Native Read/Grep will be denied (except after Edit).
Undo with: lean-ctx harden --undo
Soft harden is reversible and config-only; --hard additionally blocks the
Claude Bash tool. Both are fully undone by --undo (see also
Journey 12 → §4).
6. Role policies — least privilege per agent
What it does: role policies (context_policies.rs, set via
ctx_session action=role / lean-ctx session role) scope what a session may do
— e.g. a reviewer role that cannot write, or limiting privileged search
(ctx_search ignore_gitignore=true requires an admin-class role). Use this to
give a sub-agent or teammate a least-privilege surface.
7. Audit & alignment
lean-ctx maintains an audit trail (audit_trail.rs) of security-relevant
actions and ships an OWASP alignment map (owasp_alignment.rs) documenting
which controls address which risks — useful when answering a security review.
For a structured external yardstick, lean-ctx publishes a self-assessment against the 32-control Context Governance Benchmark (v1.0-draft): graded C2 — Managed with declared gaps — see docs/compliance/cgb-self-assessment.md.
Governance checklist
| Goal | Control |
|---|---|
| See / flip the whole posture | lean-ctx security status · yolo · secure |
| Disable containment for a trusted machine | lean-ctx yolo (keeps secret redaction on) |
| Keep file access in-project | PathJail (on by default) |
Gate a cloned repo's .lean-ctx.toml |
Workspace trust (lean-ctx trust) |
| Restrict which commands run | shell_allowlist + shell_strict_mode |
| Never wrap docker mount-escapes | docker/podman off the default allowlist |
| Confine executed code | Seatbelt (macOS) / Landlock (Linux) |
| Stop secrets reaching the model | [secret_detection] (on by default) |
| Block whole sensitivity levels (PII/secret) pre-prompt | [sensitivity] policy floor (off by default) |
| Force compressed reads | lean-ctx harden [--hard] |
| Least-privilege agents | role policies |
| Answer a security review | audit trail + OWASP alignment |
Tuning how much lean-ctx compresses or which tools it exposes lives in Journey 10 — Customization & Governance; this journey is specifically the security surface.