Repository navigation
Agent/daemon: hold the unlocked vault, serve just-in-time child-scoped secret injection #116
Description
Summary
Add an optional background agent/daemon that holds the unlocked vault in memory and serves just-in-time, child-scoped secret injection to exec / export / run. This is the foundational capability under the ergonomics work in #115 — the piece that actually attacks secret lifetime / blast-radius, which none of the #115 flags fix on their own.
Split out of #115 deliberately: #115 is the ergonomic layer (how mappings are declared and delivered); this is the substrate (where the unlocked key lives and how long a materialized secret survives). They compose; this one is the dependency.
Problem
Two gaps in today's model:
- Lifetime.
execis well-scoped — the secret dies with the child. But theexport-into-a-shell pattern proposed in Env-injection ergonomics:export/direnv,run --env-file/inject, transforms, mapping manifest #115 lives for the whole shell session (hours),/proc/<pid>/environ-readable the entire time. Shippingexportalone trades a file leak for a long-lived in-memory leak. - Unlock friction. Every
exec/getopens and decrypts the vault, so just-in-time fetching either means repeated master-password/keychain unlocks, or pushing users toward keeping things unlocked some other ad-hoc way.
A daemon resolves both: unlock once, then every fetch is fast, just-in-time, and scoped to the consuming process.
Proposal
pass-cli agent — a background process that:
- Holds the derived key / unlocked vault in memory only (never on disk).
- Serves
exec/export/run(andget) over a local socket so they fetch on demand instead of opening the vault themselves. - When the daemon isn't running, all commands fall back to today's direct-open behavior — the daemon is an optimization/safety layer, not a hard dependency.
Composed with #115: single declared source (manifest) → daemon serves JIT → materialization scoped to the child = one auditable source, zero persistent plaintext, minimal lifetime.
Design surface (the parts that need decisions)
- IPC & auth. Unix domain socket (Windows named pipe), perms
0600, plus peer-credential checks (SO_PEERCRED/getpeereid) so only same-user processes can talk to it. Define the request/response protocol (request a mapping set, return values, never log them). - Lifecycle / auto-lock. Explicit
pass-cli agent(andagent stop/lock), idle timeout and max-TTL re-lock, behavior across multiple shells, and what happens on logout/suspend. - Unlock & key handling. Unlock via master password or the existing OS keychain integration; cache the derived key in locked memory (
mlock, disable core dumps) to keep it out of swap/crash dumps. - Honest security model. The daemon's own memory is
/proc/<pid>/mem-readable by the same user — the same ceiling asssh-agent/gpg-agent. Document it; don't oversell isolation. This is strictly better than long-lived shell env or plaintext files, not a sandbox. - Integration. How it interacts with
keychain(could supply the unlock) andsync; whetherexec/export/runauto-detect and prefer the socket. - Platform service. Optional
systemd --userunit / launchd agent / Windows service, or pure run-on-demand.
Prior art
ssh-agent, gpg-agent, op daemon / op biometric session, aws-vault's server mode. The lifetime/scope model is well-trodden; this issue is about adopting it for pass-cli's vault.
Non-goals
Does not remove the consumer-side plaintext-at-boundary ceiling (an app reading process.env still gets a plaintext value at the moment of use — see #115's "hard ceiling"). This issue minimizes how long and how broadly that plaintext exists, not whether it exists at the boundary.
Relationship
- Substrate for the ergonomics layer in Env-injection ergonomics:
export/direnv,run --env-file/inject, transforms, mapping manifest #115 (export/run --env-file/ manifest become thin clients of the agent). - Implement independently; Env-injection ergonomics:
export/direnv,run --env-file/inject, transforms, mapping manifest #115's flags work without it (direct-open fallback) and get safer with it.
Filed alongside #115 from a real session wiring OpenTelemetry/Langfuse export into an eve agent; a separate Claude Code session may pick up the implementation.
Activity
refactor(exec): envmap grammar + resolver substrate + slash separator (…
feat(agent): add in-process agent core + protocol (#116 Phase 2a) (#125)
test(agent): use short socket paths (macOS 104-char unix-socket limit) (
feat(agent): unix-socket transport + agent commands + fallback (#116 …
test(agent): disable audit in test vault (sibling unlock hung on CI k…
feat(agent): revalidating cache (read-only); defer batched tracking (#…
docs(agent): service unit templates + operations guide (#116 Phase 3) (…
feat(agent): process-level memory hardening (mlockall + PR_SET_DUMPAB…
feat(agent): macOS peer-credential authorization (LOCAL_PEERCRED) (#116…
arimxyer commentedon Jul 1, 2026
Completed. The background agent/daemon shipped across merged PRs: #121 (resolver substrate), #125 (agent core + protocol, no key-export method by construction), #126 (unix-socket transport +
agent/stop/status+ socket-first-then-direct fallback), #128 (revalidating cache + LinuxSO_PEERCREDpeer-cred), #129 (mlockall+PR_SET_DUMPABLE=0memory hardening), #130 (systemd/launchd templates + docs), #131 (macOSLOCAL_PEERCRED). Post-epic lifecycle cleanup: #132 (prompt lock/stop), #134 (agent startdaemonize +agent serveforeground), #135 (removed redundantlock).Final surface:
agent start/serve/status/stop, values-only over the socket, transparent direct-open fallback when no agent runs.Deliberately scoped out (see the design doc): Windows named-pipe transport, batched usage tracking, memguard (replaced by process-level mlock). Closing as complete.
chore(release): cut v0.19.0 (#144)