{"_id":"@davidwells/op-stash","_rev":"3-efb2c99bc81a27642fdb922f6ac106de","name":"@davidwells/op-stash","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"@davidwells/op-stash","version":"0.1.0","keywords":["1password","op","secrets","cache","configorama"],"author":{"name":"David Wells"},"license":"MIT","_id":"@davidwells/op-stash@0.1.0","maintainers":[{"name":"davidwells","email":"davidgregorywells@gmail.com"}],"homepage":"https://github.com/DavidWells/configorama/tree/master/packages/op-stash#readme","bugs":{"url":"https://github.com/DavidWells/configorama/issues"},"bin":{"op-stash":"src/cli.js"},"dist":{"shasum":"f25735860bdcd751b425fd215b04835848ca3cb0","tarball":"https://registry.npmjs.org/@davidwells/op-stash/-/op-stash-0.1.0.tgz","fileCount":16,"integrity":"sha512-ryTN6yKCXz5zMl/U2NMPBxuW/ZVT7t2TpdgPZrBX0UrhtNbAOKhmC18NYiU/cGOIZAnMg/l9fHy20ahGwvtQlA==","signatures":[{"sig":"MEUCIQChvSXGMcyqPBI9l307Uqnvj0xcZzvH/OZjOkgFhWD6TgIgNnt8mrtZLXj4FYxKHuMxafyU0JRM47lTjxPXFZRBR0c=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63471},"main":"src/api.js","type":"commonjs","engines":{"node":">=22"},"gitHead":"e33072d239ed624ffb73ab3c7c05eb8347cd3afb","scripts":{"test":"node test/unit.test.js && node test/cli.test.js && node test/integration.test.js && node test/get-or-set.test.js"},"_npmUser":{"name":"davidwells","email":"davidgregorywells@gmail.com"},"repository":{"url":"git+https://github.com/DavidWells/configorama.git","type":"git","directory":"packages/op-stash"},"_npmVersion":"11.17.0","description":"Per-user in-memory cache daemon for 1Password op:// reads","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"uvu":"^0.5.6"},"_npmOperationalInternal":{"tmp":"tmp/op-stash_0.1.0_1783490330906_0.4677683660623386","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@davidwells/op-stash","version":"0.1.1","keywords":["1password","op","secrets","cache","configorama"],"author":{"name":"David Wells"},"license":"MIT","_id":"@davidwells/op-stash@0.1.1","maintainers":[{"name":"davidwells","email":"davidgregorywells@gmail.com"}],"homepage":"https://github.com/DavidWells/configorama/tree/master/packages/op-stash#readme","bugs":{"url":"https://github.com/DavidWells/configorama/issues"},"bin":{"op-stash":"src/cli.js"},"dist":{"shasum":"19aa1d0f7f7fee6e45599934c5f9fa5809e35d46","tarball":"https://registry.npmjs.org/@davidwells/op-stash/-/op-stash-0.1.1.tgz","fileCount":16,"integrity":"sha512-jE9Tndz/WHrnZPRDwlzHMLSuTHk6AjLggvZnA+8UwSqvdb/b+ekMTZvslf0PkbPsS1QaYuHOEdNDe1BKIg9SRA==","signatures":[{"sig":"MEUCIGmYApAuNJXpcr5Vsltsf4YlKuZdVlsd7aBpl0O1yXRcAiEAgTYDeqEo2AtX3VngFCX7eeVJHnL0qE53nb7/BLRlWZA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":63784},"main":"src/api.js","type":"commonjs","engines":{"node":">=22"},"gitHead":"72471d87832a75edb5e3d530cfb9408de8e8b7c7","scripts":{"test":"node test/unit.test.js && node test/cli.test.js && node test/integration.test.js && node test/get-or-set.test.js"},"_npmUser":{"name":"davidwells","email":"davidgregorywells@gmail.com"},"repository":{"url":"git+https://github.com/DavidWells/configorama.git","type":"git","directory":"packages/op-stash"},"_npmVersion":"lerna/9.0.7/node@v22.22.1+arm64 (darwin)","description":"Per-user in-memory cache daemon for 1Password op:// reads","directories":{},"_nodeVersion":"22.22.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"uvu":"^0.5.6"},"_npmOperationalInternal":{"tmp":"tmp/op-stash_0.1.1_1783490929346_0.8858144098749279","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@davidwells/op-stash","version":"0.1.2","description":"Per-user in-memory cache daemon for 1Password op:// reads","main":"src/api.js","bin":{"op-stash":"src/cli.js"},"type":"commonjs","scripts":{"test":"node test/unit.test.js && node test/cli.test.js && node test/integration.test.js && node test/get-or-set.test.js && node test/daemon-race.test.js"},"keywords":["1password","op","secrets","cache","configorama"],"author":{"name":"David Wells"},"license":"MIT","publishConfig":{"access":"public"},"homepage":"https://github.com/DavidWells/configorama/tree/master/packages/op-stash#readme","repository":{"type":"git","url":"git+https://github.com/DavidWells/configorama.git","directory":"packages/op-stash"},"bugs":{"url":"https://github.com/DavidWells/configorama/issues"},"engines":{"node":">=22"},"devDependencies":{"uvu":"^0.5.6"},"gitHead":"54014035ae878f6d6634655e87a8a21df07d1f1f","_id":"@davidwells/op-stash@0.1.2","_nodeVersion":"22.22.1","_npmVersion":"lerna/9.0.7/node@v22.22.1+arm64 (darwin)","dist":{"integrity":"sha512-ZPSDCQ9N58ij0k5qIPLFnuji7VaAF18ckPD1fkfIhGnpIgLJfXITVQWN7fHDnVd5CpZbcC1O7r7T9o8QCLD5JA==","shasum":"1e7d495e860369e23c7cf5cebadd62f3d00d875a","tarball":"https://registry.npmjs.org/@davidwells/op-stash/-/op-stash-0.1.2.tgz","fileCount":16,"unpackedSize":65792,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCtIXkGpPQ3sdjnhIyNozFlaC+cSq/XS0Q87zyl7MxWcgIgRJUwvdpCYXswXaEYKCqDGR7cbd6XR24OxjglUf7f/C0="}]},"_npmUser":{"name":"davidwells","email":"davidgregorywells@gmail.com"},"directories":{},"maintainers":[{"name":"davidwells","email":"davidgregorywells@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/op-stash_0.1.2_1783492005099_0.32121672552665714"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-08T05:58:50.693Z","modified":"2026-07-08T06:26:45.364Z","0.1.0":"2026-07-08T05:58:51.025Z","0.1.1":"2026-07-08T06:08:49.479Z","0.1.2":"2026-07-08T06:26:45.264Z"},"bugs":{"url":"https://github.com/DavidWells/configorama/issues"},"author":{"name":"David Wells"},"license":"MIT","homepage":"https://github.com/DavidWells/configorama/tree/master/packages/op-stash#readme","keywords":["1password","op","secrets","cache","configorama"],"repository":{"type":"git","url":"git+https://github.com/DavidWells/configorama.git","directory":"packages/op-stash"},"description":"Per-user in-memory cache daemon for 1Password op:// reads","maintainers":[{"name":"davidwells","email":"davidgregorywells@gmail.com"}],"readme":"# @davidwells/op-stash\n\n<p align=\"center\">\n  <em>🔐 → 🧠 → ⚡️</em>\n</p>\n\n[![npm version](https://img.shields.io/npm/v/@davidwells/op-stash.svg)](https://www.npmjs.com/package/@davidwells/op-stash)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n[![node >=22](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org)\n\n**Stop re-authorizing 1Password for every fresh process.** `op-stash` is a per-user, in-memory cache daemon for 1Password `op://` secret reads — zero dependencies, nothing ever written to disk.\n\n```bash\nnpm install -g @davidwells/op-stash\nop-stash read op://Private/MyItem/password   # prompts once, caches 5 minutes\n```\n\n## TL;DR\n\n**The problem.** 1Password CLI authorization is per-process. Agent workflows and scripts that repeatedly spawn fresh Node processes — `configx .env -- node script.js`, again and again — trigger a biometric prompt on every single run, because each process starts with no memory of the last one.\n\n**The solution.** A tiny daemon holds resolved secret values in memory, keyed by an opaque hash, behind a `0600` Unix socket only your user can touch. The first read runs `op` and prompts; every read within the TTL — from *any* process — returns instantly with no prompt. Values live for 5 minutes by default and never touch disk.\n\n| Why `op-stash` | |\n|---|---|\n| 🔁 **Cross-process** | Fresh processes share the cache — the exact thing plain `op read` can't do |\n| 🧠 **Memory only** | No cache file, no log file, no disk persistence, ever |\n| ⏱️ **Short-lived by default** | 5-minute TTL, daemon-enforced ceiling, background sweep, idle auto-exit |\n| 🔌 **Zero dependencies** | `node:crypto`, `node:net`, and nothing else in the tree |\n| 🤖 **Agent-friendly** | `--json` on every diagnostic command; secrets only ever on `read` stdout |\n| 🎛️ **Scopeable** | Partition entries per agent thread, terminal, or CI job so nothing shares by accident |\n| 🚪 **Escape hatch** | `OP_STASH_DISABLED=1` bypasses everything, instantly |\n\n## Quick Example\n\n```bash\nop-stash read op://Private/MyItem/password    # 1Password prompts, value cached\nop-stash read op://Private/MyItem/password    # instant, no prompt (cache hit)\nop-stash stats --json                          # {\"type\":\"stats\",\"entries\":1,\"hits\":1,\"misses\":1,...}\nop-stash read op://Private/MyItem/password --ttl 30m --scope session:my-agent\nop-stash clear                                 # wipe all cached values now\nop-stash stop                                  # kill the daemon (and everything in it)\n```\n\n## How It Works\n\n```text\n┌─────────────┐  1. get(key)   ┌──────────────────┐\n│ your process │──────────────▶│  op-stash daemon │   in-memory TTL cache\n│  (op-stash / │◀──────────────│  (per-user, 0600 │   never runs op\n│  API caller) │  hit: value    │   unix socket)   │   never touches disk\n└──────┬───────┘                └──────────────────┘\n       │ miss\n       ▼ 2. op read --no-newline <ref>     ◀── 1Password prompt happens HERE,\n┌─────────────┐                                 in YOUR process, so the app\n│    op CLI   │                                 attributes it to your terminal\n└──────┬───────┘\n       │ 3. set(key, value, ttl)  ──▶  daemon stores it for the next process\n       ▼\n     stdout\n```\n\nThe load-bearing design rule: **the daemon never executes `op`**. Cache misses are resolved by the client process, so 1Password's app integration, terminal attribution, and authorization prompts behave exactly as they do without op-stash. The daemon is only a short-term memory.\n\n## Design Principles\n\n1. **Memory is the only storage.** No value, ref, or log line ever reaches disk. Kill the daemon, the secrets are gone.\n2. **The client does the secret work.** `op` runs in your process; the daemon is a dumb store that can't reach 1Password at all.\n3. **Opt-in everywhere.** Nothing enables caching implicitly — not installation, not the Configorama plugin, nothing. You write `cache: { provider: 'op-stash' }` or it doesn't happen.\n4. **Fail loudly, degrade explicitly.** A configured-but-broken cache errors by default; `fallbackToOp: true` is the deliberate opt-in to silent degradation.\n5. **Bounded lifetime beats encryption.** Encrypting values inside the daemon would put the key in the same memory the attacker would read — theater. Short TTLs, sweeps, and idle exit actually shrink the exposure window. (See [Security Model](#security-model).)\n\n## vs Alternatives\n\n| | `op-stash` | Plain `op read` | `.env` file on disk | `OP_SERVICE_ACCOUNT_TOKEN` |\n|---|---|---|---|---|\n| Prompts per fresh process | First one only (per TTL) | Every process | None | None |\n| Secrets on disk | Never | Never | **Plaintext, forever** | Token in env/shell rc |\n| Secret lifetime | Minutes (TTL) | Instantaneous | Unbounded | Token lifetime |\n| Works offline after first read | Within TTL | No | Yes | No |\n| Setup | `npm i -g`, nothing else | Nothing | Manual copying | Service account admin |\n| Best for | Interactive agent loops | One-off reads | Nothing, honestly | Headless CI |\n\n## Install\n\n```bash\n# global CLI\nnpm install -g @davidwells/op-stash\n\n# or as a project dependency (programmatic API + local bin)\nnpm install @davidwells/op-stash\npnpm add @davidwells/op-stash\n```\n\nRequires Node >= 22 and the [1Password CLI](https://developer.1password.com/docs/cli/get-started/) (`op`) on `PATH`. macOS and Linux; on Windows every command transparently falls through to direct `op` execution with no caching.\n\n## Quick Start\n\n1. Install: `npm install -g @davidwells/op-stash`\n2. Read a secret: `op-stash read op://<vault>/<item>/<field>` — approve the 1Password prompt\n3. Read it again — no prompt. That's the whole product.\n4. Check what's cached: `op-stash stats --json`\n5. Done for the day: `op-stash stop`\n\n## Commands\n\n```text\nop-stash read <op://ref> [--account <account>] [--ttl <duration>] [--scope <scope>]\nop-stash status [--json]\nop-stash stats [--scope <scope>] [--json]\nop-stash clear [--scope <scope>]\nop-stash stop\nop-stash doctor [--json]\nop-stash config-path\n```\n\n| Command | What it does | Example |\n|---|---|---|\n| `read` | Resolve a secret through the cache. The **only** command that starts the daemon. Secret goes to stdout, everything else to stderr | `op-stash read op://Private/npm/credential --ttl 1h` |\n| `status` | Daemon running? Version, socket path, effective config | `op-stash status --json` |\n| `stats` | Entry count, hits, misses, hit rate. With `--scope`, live entry count for that scope | `op-stash stats --scope session:ci --json` |\n| `clear` | Remove all entries, or one scope's. Exits 0 when the daemon isn't running — nothing-to-clear isn't an error | `op-stash clear --scope pid` |\n| `stop` | Shut the daemon down; all cached values vanish with it | `op-stash stop` |\n| `doctor` | Environment diagnosis: Node/op versions, socket safety, config path, daemon/client version mismatch, whether `OP_SERVICE_ACCOUNT_TOKEN` is set (never its value) | `op-stash doctor --json` |\n| `config-path` | Print the resolved config file location | `op-stash config-path` |\n\n## TTL\n\n```bash\nop-stash read op://Private/MyItem/password --ttl 5m\n```\n\nDurations: `30s`, `5m`, `1h`, or bare seconds (`300`). Three places set TTL, most specific wins: per-request `--ttl` / `ttlSeconds`, then `OP_STASH_TTL_SECONDS` / config file, then the 300-second default. The daemon clamps anything above its `max_ttl_seconds` (default 24h) and warns once on stderr. Reading a value does **not** extend its clock — after the TTL it's gone and the next read re-prompts.\n\n## Scopes\n\nScopes partition the cache so different contexts never share secrets by accident:\n\n```bash\nop-stash read op://Private/MyItem/password --scope user                      # default: shared per user\nop-stash read op://Private/MyItem/password --scope session:claude-thread-abc # explicit namespace\nexport OP_STASH_SCOPE=\"session:claude-thread-abc\"                            # same, via env\nop-stash clear --scope session:claude-thread-abc                             # scoped wipe\n```\n\n- `user` — shared by all same-user callers using the same account/config/op path (default)\n- `session` — resolved from `OP_STASH_SESSION`, then `OP_STASH_SCOPE`; bare `session` with neither set is an error (never silently falls back to a shared scope)\n- `session:<name>` — inline session namespace; recommended for agents, where a controlling process sets one stable value per thread\n- `pid` / `ppid` — tied to a process id; entries can be dropped when the owner dies\n- any other string — custom scope, you own the namespace\n\n## Configuration\n\nPrecedence: **CLI flags → `OP_STASH_*` env vars → config file → defaults.**\n\nOptional config file at `~/.config/op-stash/config.json`:\n\n```json\n{\n  \"socket_path\": \"/tmp/op-stash-501.sock\",\n  \"ttl_seconds\": 300,\n  \"max_ttl_seconds\": 86400,\n  \"max_entries\": 1000,\n  \"op_path\": \"op\",\n  \"op_timeout_seconds\": 30,\n  \"default_scope\": \"user\",\n  \"idle_exit_seconds\": 1800\n}\n```\n\n| Env var | Meaning | Default |\n|---|---|---|\n| `OP_STASH_SOCKET_PATH` | Daemon socket location | `$TMPDIR/op-stash-<uid>.sock` |\n| `OP_STASH_TTL_SECONDS` | Default entry lifetime | `300` |\n| `OP_STASH_MAX_TTL_SECONDS` | Daemon-enforced TTL ceiling | `86400` |\n| `OP_STASH_MAX_ENTRIES` | LRU eviction threshold | `1000` |\n| `OP_STASH_OP_PATH` | 1Password CLI binary | `op` |\n| `OP_STASH_OP_TIMEOUT_SECONDS` | Kill slow `op` calls after | `30` |\n| `OP_STASH_IDLE_EXIT_SECONDS` | Empty + idle daemon exits after | `1800` |\n| `OP_STASH_SCOPE` | Default scope for reads | `user` |\n| `OP_STASH_SESSION` | Dedicated session-scope id (beats `OP_STASH_SCOPE`) | — |\n| `OP_STASH_DISABLED` | `1` = bypass the daemon entirely, run `op` directly | — |\n\nThe daemon reads config at startup; clients read it every invocation. `status` reports the daemon's version and effective config, and warns on mismatch. Reload = `op-stash stop`, next read starts fresh.\n\n## Configorama / configx\n\n`configx` stays a runner — cache integration belongs in the Configorama 1Password resolver, and it's strictly opt-in:\n\n```js\n// configx.config.js\nconst createOnePasswordResolver = require('configorama/plugins/onepassword')\n\nmodule.exports = {\n  variableSources: [\n    createOnePasswordResolver({\n      cache: {\n        provider: 'op-stash',\n        ttlSeconds: 300,\n        scope: process.env.OP_STASH_SCOPE || 'user'\n      }\n    })\n  ]\n}\n```\n\nWith the cache configured, **every** supported `${op...}` syntax caches across fresh processes: direct `op://` refs, aliases, item names and IDs, private links, explicit and inferred fields, and structured note key paths. Direct refs share keys with `op-stash read`; everything else caches its final resolved value. An unrecognized `provider` string errors immediately — a typo can't silently disable a secrets cache you asked for.\n\n## Programmatic API\n\n```js\nconst { read, getOrSet, status, stats, clear, stop } = require('@davidwells/op-stash')\n\nconst value = await read('op://Private/MyItem/password', {\n  ttlSeconds: 300,\n  scope: 'user',\n  fallbackToOp: false\n})\n```\n\n### getOrSet (advanced)\n\nGet-or-compute for integrations whose miss producer is their own logic rather than `op read`. API-only — producers are functions, so there is no CLI form.\n\n```js\nconst value = await getOrSet('my-tool://v1/<hash>', async () => {\n  return computeTheValueSomehow() // must return a string\n}, {\n  account,\n  configDir,\n  opPath,\n  ttlSeconds: 300,\n  scope: 'user',\n  fallbackToOp: false,\n  validateCached: (cached) => looksValid(cached)\n})\n```\n\n- any non-empty reference string is accepted; `account`/`configDir`/`opPath` are applied as cache key dimensions exactly as for `read`\n- `OP_STASH_DISABLED=1` and win32 run the producer directly, never touching the daemon\n- on a hit, `validateCached(value)` runs first when provided; returning `false` or throwing treats the entry as unusable — the producer recomputes and overwrites it, with a one-line stderr note at most once per process\n- the producer must return a string; anything else throws and stores nothing\n- producer failures surface unchanged and are never cached\n- `fallbackToOp: false` (default) fails closed on daemon failure before the producer runs; `true` means \"on daemon failure, do the work directly\", with the same stderr warning style as `read`\n- TTL clamping by the daemon warns at most once per process\n\n## Daemon Lifecycle\n\nOnly `read` (and the API equivalents) starts the daemon — `status`, `stats`, `clear`, `stop`, and `doctor` never spawn one just to report an empty cache. The daemon exits on its own after `idle_exit_seconds` once the cache is empty, sweeps expired entries every minute, and removes its socket and pid file on shutdown.\n\n## Security Model\n\n**Where the secret lives:** in the daemon process's heap, full stop. The socket file is a zero-byte rendezvous point. Nothing is written to disk and nothing is logged.\n\n**Who can read it:**\n\n- *Other users*: no. Kernel-enforced `0600` socket, uid-suffixed path, and both sides verify ownership — the client refuses unsafe sockets, the daemon refuses to bind over suspicious paths.\n- *Processes running as you*: yes, by design — that is the feature. Any same-user process that knows the ref and key dimensions can fetch the value without a prompt during the TTL window. This is the same trust model as `ssh-agent`, `gpg-agent`, and the 1Password CLI's own session state.\n- *Root*: can read any process's memory. No userland tool changes that.\n\n**Why values aren't encrypted:** the decryption key would have to live in the same memory an attacker would be reading, and the daemon must return plaintext to legitimate clients anyway. Encryption there changes which bytes get copied, not who can copy them. What actually bounds exposure: short TTLs, the daemon-side ceiling, the minute-interval sweep, idle exit, and `clear`/`stop` for instant wipe.\n\n**Hygiene guarantees:** stats and diagnostics never contain refs or values (only truncated hashes); raw private-link URLs never reach the daemon; secret values appear on stdout only for `read`; the socket never touches the network.\n\nThe one behavioral change to internalize: **during the TTL window, possession of your user account means silent access to cached values.** Scope namespaces, short TTLs, and `OP_STASH_DISABLED=1` exist for contexts where that trade isn't right.\n\n## Troubleshooting\n\n| Symptom | Cause | Fix |\n|---|---|---|\n| `daemon not running` from status/stats | Nothing has called `read` yet, or idle exit fired | Expected — only `read` spawns the daemon |\n| `Unsafe op-stash socket ...` | Socket exists with wrong owner/permissions, or a non-socket file squats the path | Remove the offending file; check `OP_STASH_SOCKET_PATH` |\n| `Socket path exceeds 100 bytes` | Unix sockets cap path length (~104 bytes on macOS) | Point `OP_STASH_SOCKET_PATH` somewhere shorter, e.g. `/tmp` |\n| `ttl clamped to Ns by daemon max_ttl_seconds` | You asked for a longer TTL than the daemon allows | Raise `max_ttl_seconds` (config/env) and `op-stash stop` to reload |\n| `daemon version X differs from client` | Package upgraded while an old daemon kept running | `op-stash stop`; next read starts the new version |\n| `cache bypassed (...)` on stderr | Daemon failure with `fallbackToOp: true` — value came from direct `op` | Run `op-stash doctor --json`; often a stale socket |\n| Still getting 1Password prompts | TTL expired, scope mismatch, `OP_STASH_DISABLED` set, or the value legitimately was never cached | `op-stash stats --scope <scope>` to see what's actually in there |\n\n## Limitations\n\n- **Not a hard security boundary.** Same-user processes can read cached values without a prompt during the TTL. If that's unacceptable for a context, don't enable the cache there.\n- **Concurrent cold misses both prompt.** Two processes missing the same key simultaneously each run `op`. The target workflow is sequential agent commands, so v1 accepts this rather than adding request coalescing.\n- **No Windows caching.** win32 falls through to direct `op` execution — configs stay portable, Windows users just keep their prompts.\n- **JS strings can't be zeroed.** A cleared value may briefly survive in V8's garbage-collected heap — true of every Node process that ever touches a secret.\n- **Wrong tool for servers/CI.** Use `OP_SERVICE_ACCOUNT_TOKEN` for headless environments; this exists for interactive, biometric-prompt workflows.\n\n## FAQ\n\n**How long do secrets stay cached?**\n5 minutes by default. Per-read `--ttl` up to the daemon's `max_ttl_seconds` (24h default). A background sweep deletes expired entries within ~60 seconds of expiry.\n\n**Does anything ever hit disk?**\nNo. No cache file, no log file, no swap-file writes by the daemon itself. macOS encrypts swap by default if the OS ever pages the heap.\n\n**Why did my read not prompt even though the cache was cold?**\n1Password's own session behavior: once the desktop app has authorized your terminal and remains unlocked, `op` calls are silently approved. The cache's job is the cases where that *doesn't* hold — fresh process attribution, locked app, agent harnesses.\n\n**Can I use different 1Password accounts?**\nYes — `--account` is a cache key dimension, so accounts never share entries. Same for `configDir` and the `op` binary path.\n\n**What happens if the daemon dies mid-read?**\nThe CLI retries once (respawning if needed), then falls through to direct `op read` with a stderr warning. The programmatic API fails closed unless you pass `fallbackToOp: true`.\n\n**How do I wipe everything right now?**\n`op-stash clear` (keep daemon) or `op-stash stop` (kill it all). Both work instantly.\n\n**Is my `op-stash read` slower than plain `op read`?**\nA hit is dramatically faster (no `op` process, no 1Password round-trip). A miss adds ~a few milliseconds of socket round-trip on top of the normal `op` call.\n\n## Prior Art\n\nThe [Rust op-cache](https://github.com/SamSaffron/op-cache) by Sam Saffron is the reference and inspiration for this package. op-stash ports its user-facing behavior to Node with per-request TTLs, scopes, the `getOrSet` integration API, and Configorama support.\n\n## License\n\nMIT © [David Wells](https://davidwells.io)\n","readmeFilename":"README.md"}