Secrets
Anything LatchAI automates eventually needs a credential. The rule the secret store enforces is simple: a secret’s value never enters a file you might commit, an event you might share, or a prompt you might send to a model.
Where secrets live
Section titled “Where secrets live”On macOS, secrets are stored in the login Keychain via the system security CLI — not in
the workspace, not in git, not in any LatchAI file. Each one is a generic password under the
service latchai, with the secret’s name as the account, so security sees them too:
security find-generic-password -s latchai -a MY_TOKEN -w # prints the value (Keychain may prompt)A names-only index (secrets.index.json, in the LatchAI home)
lets LatchAI list what exists without exposing values.
Off macOS, or if the Keychain is unavailable, the store falls back to secrets.local.json
in the home, written with 0600 permissions. That fallback is a real file with real values
in it, so treat it accordingly — and exclude it if you ever put your home under version
control. GET /api/secrets reports which backend is live (keychain or file) alongside
the names.
Managing them
Section titled “Managing them”The secrets manager is a command-palette away: ⌘⇧P → Secrets: Manage. It lists the
names it knows, takes a new name and value, and deletes. It never displays a value — the
API has no read endpoint for the UI to call, by construction.
- Names are
[A-Za-z0-9_.-]. A name outside that set is rejected rather than escaped. - Rotating is adding the same name again; the Keychain entry is updated in place, and
every
{{secret:NAME}}reference picks up the new value on its next use with no edits. - Deleting removes the entry and the index line. References to it start failing loudly (see Troubleshooting) rather than quietly sending an empty string.
Referencing a secret
Section titled “Referencing a secret”Secrets are referenced as {{secret:NAME}}. The reference is what gets stored; the value
is substituted by the executor at the moment of use — in an HTTP node’s URL, headers,
and body, and in an MCP server’s stdio environment values and HTTP
header values. The value therefore never reaches the workflow JSON on disk, the run event
log, or the model’s context window.
{ "type": "http_request", "config": { "url": "https://api.example.com/v1/things", "headers": { "Authorization": "Bearer {{secret:EXAMPLE_TOKEN}}" } }}Substitution happens after {{nodeId}} interpolation and immediately before the call, so
an upstream node’s output can build the request while the credential is still added last.
Two more defences on the same node. The response is scanned for any secret value the call
used and those occurrences are replaced with [redacted], so a chatty API that echoes your
token back can’t leak it into a node output that a model then reads. And a failed request
reports the host only — POST api.example.com -> 401 — never the full URL, which may
carry a token in its query string.
The same referencing works in mcp.json, which is what keeps tokens out of a file you’d
otherwise happily paste into a bug report:
{ "servers": { "tracker": { "transport": "http", "url": "https://mcp.example.com/mcp", "headers": { "Authorization": "Bearer {{secret:TRACKER_TOKEN}}" } } }}Provider API keys
Section titled “Provider API keys”Keys entered in the Models page’s Connected rows are
written to the secret store and referenced from latchai.config.json as {{secret:...}}
rather than stored inline. Keys you don’t touch on a later save carry over automatically,
so the UI never has to echo a key back to you to preserve it. The secret is named for the
provider — provider.<name>.apiKey — so what you see in the config is a pointer:
{ "providers": { "openai": { "type": "openai", "baseUrl": "https://api.openai.com/v1", "model": "gpt-5.2", "apiKey": "{{secret:provider.openai.apiKey}}" } }}Agent shell
Section titled “Agent shell”run_shell executes with a secret-scrubbed environment: every variable whose name
looks credential-shaped (SECRET, TOKEN, PASSWORD, PASSWD, API_KEY, CREDENTIAL,
in any case) is dropped before the command runs. An agent can read env freely and still
not find your keys there; anything it legitimately needs must come from the secret store by
name.
It also runs with a git ceiling at the LatchAI home, so git discovery inside workspace/
cannot walk up out of it — an agent’s git add -A stages its own project, never LatchAI’s.
Mounted repositories are their own git toplevels and are unaffected.
Webhook secrets
Section titled “Webhook secrets”A webhook_trigger may set config.secret; callers present it
with the x-latchai-secret header or a ?secret= query parameter. Combined with the engine’s
loopback bind and non-local Host/Origin rejection, a webhook is local-only unless you
deliberately change the bind address.
If you do expose a hook beyond the machine:
- Set
config.secretfirst. It is the only authentication the hook has; the comparison is constant-time, so neither the value nor its length leaks through timing. - Terminate TLS in front of it. The engine speaks plain HTTP; a bare
LATCHAI_BINDon a LAN address sends that token in the clear. - Keep the blast radius small. The workflow behind the hook runs with whatever tools you gave it — put the narrowest allowlist you can on any agent node it reaches.
- Watch it. Every fire is a
run.startedwithsource: "webhook", so unexpected traffic is visible in the Run Monitor and the run logs.
Troubleshooting
Section titled “Troubleshooting”A missing secret fails differently in each of the three places it’s used, and knowing which message belongs to which saves a lot of guessing:
- In a workflow —
http_requestthrowsunknown secret: NAMEand the node fails. The run stops; nothing is sent. - In a provider — the engine can’t fail the boot over it, so it logs
[latchai] SECRET RESOLUTION FAILED … the raw placeholder will be sent as the API keyand carries on. The symptom downstream is every model call returning 401. Check the daemon’s console (or<home>/runs/daemon.log) before you go looking at the provider’s status page. - In an MCP server — connection is refused with
secret not set: NAME — add it in Secrets, then re-save, and that server’s tools are simply absent from the registry.