Security & Privacy
No telemetry, no cloud requirement
Glomeris has no backend, no account system, and no telemetry. Every command
documented in this book runs entirely locally. The only network access anywhere
in the codebase is to whatever BYOK endpoint you configure yourself, from
exactly two commands — glomeris llm-plan (the planner) and glomeris llm-check (the connection test, which sends two fixed words and nothing about
this machine). See BYOK LLM Planner. Nothing else in the crate makes
a network request.
No raw filesystem inventory, and no filesystem paths, sent to any LLM
As of HORO-1008, glomeris llm-plan is wired into the binary (see
BYOK LLM Planner). No filesystem data is sent anywhere unless you
explicitly run that subcommand without --plan-file and have all three
provider settings configured — every other command in this book either makes no
network request at all, or (glomeris llm-check) sends two fixed words that
describe nothing local. Since HORO-1309 those three settings may come from the
menu-bar app’s Settings rather than from GLOMERIS_LLM_* in a shell, which
changes where they are stored and nothing about what is sent: the GUI’s AI Plan
card spawns the same subcommand with the same bounded payload. Even then, what is sent is bounded and
explicit: LlmResourceView is a hand-maintained projection of one
Evidence record containing a resource’s kind, size estimate, age,
regenerability, completeness, and the action ids offered for it — never raw
file contents, never a directory listing, never anything beyond that fixed
set of fields. Adding a field to Evidence later has no effect on what a
model sees unless a human explicitly adds it to LlmResourceView too, and
adding a field to LlmResourceView itself fails a test that pins its
serialized key set.
Nor are resource paths sent. Until HORO-1298 each view identified its
resource by its real ResourceId, which for the five path-backed resource
kinds renders as an absolute path — and an absolute path under $HOME
discloses the OS account name and the machine’s directory layout. Note that
--project-root never bounded this: it scopes the cargo and node detectors
only, while the Xcode detector is $HOME-bounded and the Homebrew and
Docker detectors shell out and are bounded by neither. Each view now
carries a positional wire alias — resource_1, resource_2, … — and the
table mapping an alias back to a real ResourceId has no Serialize
derive and stays in the process’s memory. The aliases are positional rather
than hashed on purpose: a hashed path would be both brute-forceable (one
unknown segment in /Users/<name>/Library/...) and a stable handle for
correlating your machine across requests.
Run glomeris llm-plan --print-payload to see the exact request a live run
would send, including the local alias table, without sending it and without
configuring a credential. Three tests enforce the property, the strongest
of them (tests/llm_plan_egress_privacy.rs) by capturing the real HTTP
request with a loopback listener and asserting the transmitted bytes
contain no path separator at all.
BYOK secret handling
OpenAiCompatibleProviderdeliberately does not deriveDebug, so an accidental{:?}-log of the provider value cannot leak the API key.- Verified by test: neither
LlmError’sDebugoutput nor a real failedcomplete()call’s error output contains the key. glomeris llm-planandglomeris llm-checkread the key only fromGLOMERIS_LLM_API_KEYviaactions::llm::provider_from_env—std::env::varfor this value is called nowhere else in the crate. The key is never accepted as a CLI flag:--api-key/--key/--tokenare explicitly rejected with an error pointing at the environment variable instead, so a key never appears inpsoutput or shell history. The rejection message names the flag only, never the value beside it, so--api-key=<secret>does not get echoed back. See the BYOK page for the full configuration contract.- In the menu-bar app (HORO-1309) the key lives in the login keychain, not
in
UserDefaults, not in a file the app writes, and not in a log line. It is read at the moment aglomerischild process is spawned, placed in that child’s environment, and not cached.GlomerisLlmSettingsStoredeliberately exposes no getter for it — the only code that can read the value is the one function that builds a child environment — and the settings screen binds it to aSecureFieldwith no reveal control and no read-back. Removing it is an explicit destructive control, not a side effect of clearing a field.scripts/check-credential-store-uses-keychain.shenforces all of that in CI over every line in the app that touches the key, so a future shortcut — stashing it in a plist “just for now”, printing it in a debug view — fails a check rather than shipping.
Subprocess invocation
Every external tool Glomeris shells out to — docker, brew, lsof,
git, pgrep, launchctl, cargo, npm/pnpm/yarn, brew (as an
action) — is invoked via std::process::Command::new(program).args(args)
with a Vec<String>/&[&str] argument array. There is no sh -c anywhere
in this codebase, and no place where untrusted content is interpolated into
a shell command string.
The one place a string is built and passed to an external tool is
platform::macos::notify’s osascript invocation, which constructs an
AppleScript source snippet via format!. That string’s only two
substituted values are the notification title and body produced by
monitor::notifier::notification_text, which only ever emits fixed template
text derived from the closed PressureState enum — never external or
attacker-controlled input — and the whole script is still passed to
osascript as a single argument in an argument array, never through
sh -c.
No arbitrary LLM-composed commands reach execution
ActionPlan/ActionStep deliberately never implement Deserialize, so no
externally-sourced input (a parsed LLM response, a network payload) can ever
construct one directly. The only way external input reaches execution is by
selecting a pre-registered ActionId string, which a real Action::plan
implementation then interprets against Evidence this process already
collected and trusts independently. See Safety Model for
how this interacts with policy classification.
Bounded, printable failure reporting
EmergencyReport caps the number of retained error strings at 8, and no
type in this crate’s execution/reporting path derives Serialize in a way
that would let an unbounded or adversarial input grow a report without
limit.