Protection Env File¶
protection enable writes tool base-URL environment variables to
~/.lumen-argus/env. That file is sourced by your shell on every
startup through a one-line source block in ~/.zshrc / ~/.bashrc.
The file has two body shapes, picked by the caller via
--managed-by (or enable_protection(..., managed_by=...) in
Python):
| Mode | Caller | Body | Failure mode it defends |
|---|---|---|---|
cli (default) |
User running the binary from a terminal (source install, pip, brew) |
Unconditional exports. The user owns the lifecycle — uninstall means running protection disable or setup --undo. |
None. No silent-removal failure mode exists in this workflow. |
tray |
Desktop tray app sidecar + enrollment flow | Self-healing liveness guard. Exports activate only when the tray-app bundle recorded in ~/.lumen-argus/.app-path still exists or the enrolled relay PID is alive. |
A dragged-to-Trash tray app silently pointing AI tools at a dead 127.0.0.1 proxy. |
lumen-argus does not infer the mode from the environment (no
parent-process sniffing, no Docker/brew/pip detection). The invoker
states the mode explicitly. The chosen mode is recorded in the file
header and echoed back in the status dict (managed_by field) so
callers can verify the file they are looking at is the one they wrote.
Mode is sticky¶
Once a mode is recorded in the file header, downstream mutators that
do not know the mode (setup, add_env_to_env_file, etc.) preserve
it. Running lumen-argus-agent setup on an enrolled machine will not strip
the liveness guard — a write with no explicit --managed-by reads the
existing header and keeps the same shape. Changing the mode requires
an explicit protection enable --managed-by ….
Status contract¶
lumen-argus-agent protection status returns a JSON document with the
following keys:
| Key | Type | Meaning |
|---|---|---|
enabled |
bool | true iff the env file has at least one managed export line |
env_file |
string | absolute path to ~/.lumen-argus/env |
env_vars_set |
int | number of managed exports in the file |
managed_by |
"cli" / "tray" / null |
mode recorded in the file header; null when disabled or the file lacks a recognised header |
The same keys plus managed_by also come back from protection
enable. A tray-app or dashboard consumer that persists "I last wrote
this file as X" compares X against managed_by here to detect drift.
File location and permissions¶
| Path | Mode | Owner | Purpose |
|---|---|---|---|
~/.lumen-argus/env |
0o600 |
the user | sourced by the shell |
~/.lumen-argus/.env.lock |
0o600 |
the user | fcntl lock held during writes |
~/.lumen-argus/.app-path |
0o600 |
the user | tray app bundle path marker (written by the desktop app on every launch) |
~/.lumen-argus/enrollment.json |
0o600 |
the user | enrollment proof (dedicated mode only) |
~/.lumen-argus/relay.json |
0o600 |
the user | live relay state (dedicated mode only) |
0o600 on the env file is load-bearing: the shell sources it as
trusted code, so group- or world-writable permissions would be an
arbitrary-code-execution vector.
Source block¶
A single block in your shell profile sources the env file when it exists. It is idempotent and written exactly once:
# lumen-argus:begin
[ -f "$HOME/.lumen-argus/env" ] && source "$HOME/.lumen-argus/env"
# lumen-argus:end
CLI mode body (--managed-by cli, default)¶
Straight export lines. The header tags the mode so a reader of the
file can see which lifecycle it belongs to.
# lumen-argus:managed-env (cli) — do not edit manually
export ANTHROPIC_BASE_URL=http://127.0.0.1:8070 # lumen-argus:managed client=claude_code
export OPENAI_BASE_URL=http://127.0.0.1:8070 # lumen-argus:managed client=aider
No guard — the exports take effect unconditionally. This is the
correct default for a user who cloned the repo, ran uv sync, and
started the proxy themselves: they started it, they can stop it, and
they do not need the guard.
Tray mode body (--managed-by tray)¶
protection enable --managed-by tray (used by the desktop tray app
sidecar and by the enrollment flow) wraps the exports in a pure-shell
liveness guard:
# lumen-argus:managed-env — do not edit manually
_la_active=0
if [ -f "$HOME/.lumen-argus/enrollment.json" ] && [ -f "$HOME/.lumen-argus/relay.json" ]; then
_la_pid=
while IFS= read -r _la_line; do
case $_la_line in
*'"pid":'*)
_la_pid=${_la_line##*: }
_la_pid=${_la_pid%,}
break
;;
esac
done < "$HOME/.lumen-argus/relay.json"
[ -n "$_la_pid" ] && kill -0 "$_la_pid" 2>/dev/null && _la_active=1
unset _la_pid _la_line
fi
if [ "$_la_active" = "0" ] && [ -f "$HOME/.lumen-argus/.app-path" ]; then
read -r _la_app < "$HOME/.lumen-argus/.app-path"
[ -n "$_la_app" ] && [ -d "$_la_app" ] && _la_active=1
unset _la_app
fi
if [ "$_la_active" = "1" ]; then
export ANTHROPIC_BASE_URL=http://127.0.0.1:8070 # lumen-argus:managed client=claude_code
export OPENAI_BASE_URL=http://127.0.0.1:8070 # lumen-argus:managed client=aider
fi
unset _la_active
The header tag (tray) is the audit marker — a reader can tell at a
glance which lifecycle this file belongs to.
protection disable truncates the file to empty bytes regardless of
mode; the source block in your shell profile is a no-op on an empty
file. On macOS the same call also runs launchctl unsetenv for each
managed variable name so GUI-launched AI tools (Claude Desktop,
Cursor) stop pointing at the proxy immediately — not just newly
spawned terminals. The list of cleared names comes back in the
status dict as launchctl_vars_cleared (empty on non-Darwin).
Ordering inside disable_protection is load-bearing: read the env
file → truncate → clear launchctl. A crash after the truncate and
before the launchctl call leaves the shell env empty while
launchctl still carries stale values, which is strictly better than
the inverse (shell active, launchctl empty) — the latter would cause
GUI AI tools to hit the dead proxy without a matching shell symptom
the user could investigate.
Guard semantics (tray mode only)¶
Two independent checks, in order:
-
Dedicated mode — if
enrollment.jsonandrelay.jsonboth exist, parse the relay PID fromrelay.jsonand probe it withkill -0. A live PID means the relay service is intercepting traffic on behalf of the user's organization, so the exports stay on even when the tray app itself has been removed. -
Local mode — if the dedicated check did not activate, read
.app-pathand check that the recorded bundle directory still exists..app-pathis rewritten by the tray app on every launch, so dragging the app to Trash makes the path dangle immediately and the guard stops exporting.
If neither check activates, the exports are skipped entirely and the
shell keeps whatever ANTHROPIC_BASE_URL etc. the user already had.
Activation matrix (tray mode)¶
| Tray app bundle | Enrollment | Relay PID | Exports? | Effective route |
|---|---|---|---|---|
| present | — | — | yes | via proxy / relay |
| missing | missing | — | no | direct to provider |
| missing | present | alive | yes | via relay |
| missing | present | dead | no | direct to provider |
| missing | present | relay.json missing |
no | direct to provider |
CLI mode does not have an activation matrix — exports are always on while the file exists.
Zero-subprocess parser¶
The guard uses only shell builtins — no python3, no jq, no grep
subprocess. PID extraction leans on the stable shape of
json.dump(…, indent=2) output: one key per line, "key": value
separator. The parser walks the file with while read, matches
*'"pid":'* in a case, and strips the value with two parameter
expansions:
_la_pid=${_la_line##*: } # drop everything up to and including "key": "
_la_pid=${_la_pid%,} # drop trailing comma (none if pid is last key)
Total cost on a cold shell: two or three stat() syscalls plus one
small file read — well under 1 ms in every mode. The previous
curl-probe and python3-parse variants were discarded because they
added tens to hundreds of milliseconds to every interactive shell
startup.
Managed-line format¶
Each active export is tagged for clean identification and undo:
(In tray mode the line is two-space indented inside the if ...; then
block; read_env_file() tolerates both shapes.)
The double-space before the tag is load-bearing — read_env_file()
uses it as the delimiter that separates the value from the managed
marker. Lines without a client= suffix are recognised as
orphans: preserved on read/write round-trip, evicted when a
canonical entry for the same variable is written.
Choosing a mode¶
| You are… | Use |
|---|---|
A user running the proxy from a terminal (source, pip, brew) |
--managed-by cli (the default — no flag needed) |
| The desktop tray app writing the env file through its agent sidecar | --managed-by tray |
The enrollment flow (lumen-argus-agent enroll) |
--managed-by tray — handled automatically |
| A Docker deployment | Neither — ~/.lumen-argus/env is not used inside containers. Set ANTHROPIC_BASE_URL directly in your compose / host shell. |
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
| Tools reach provider directly in tray mode even with tray app running | .app-path missing or stale |
Launch the tray app once — it rewrites .app-path |
| Tools reach provider directly after enrollment | relay.json missing or PID dead |
lumen-argus-agent relay (or restart the relay launchd service) |
echo $ANTHROPIC_BASE_URL empty in a new terminal right after install |
New terminals inherit env at login; the source block runs at shell startup | Open a new terminal after the source block was added, or source ~/.lumen-argus/env manually |
echo $ANTHROPIC_BASE_URL empty but the CLI proxy is running |
Env file was written with --managed-by tray but nothing wrote .app-path / relay.json |
Re-run lumen-argus-agent protection enable (default --managed-by cli — unconditional exports) |
| Env file exists but is empty | protection disable truncated it |
lumen-argus-agent protection enable |
Related¶
- Layer 3 of the clean-uninstall spec covers the five-layer design that this env file participates in.
- Client detection and setup explains how the env file is populated from the client registry.
- CLI reference documents
lumen-argus-agent protection {enable,disable,status}andlumen-argus-agent uninstall. lumen-argus-agent uninstallis the clean removal path that combinesprotection disablewith full config revert and data-file cleanup; see the CLI reference for its structured JSON output.