From 5b328e6bdb5b97f6d1e533493cf022ca2b618545 Mon Sep 17 00:00:00 2001 From: Ari Mayer Date: Fri, 26 Jun 2026 20:01:47 -0400 Subject: [PATCH] docs: document exec + safe-default list, refresh CHANGELOG for 0.18.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two user-facing changes shipped without docs and are now covered ahead of the 0.18.0 release: - exec (#98): new command reference entry, and scripting-guide rewritten to lead with `pass-cli exec` as the recommended way to hand a credential to a child command (env injection, never touches files/clipboard/history). The old `$(pass-cli get ...)` substitution is kept as a documented fallback with an explicit shell-tracing/CI-log leak caveat. README gains exec examples. - list safe-by-default (#95, #97): documented that the table now hides the Username column by default, plus the new --show-usernames and -q/--quiet flags, across command-reference, scripting-guide, and README. Also: - CHANGELOG: populate the long-stale [Unreleased] (top entry was 0.17.2) with everything since — exec, list-safe, sync content-hash markers (#102), unlock-latency overlap (#103), startup-latency cleanup (#104), tracked mise.toml (#100), CI-on-all-PRs (#105), agent skill (#101) — and head it as [0.18.0]. - CLAUDE.md: refresh the Sync Architecture notes for the #102 marker-based change detection and the #103 overlapped pre-unlock pull. No code changed. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_019sxsM218vNzDbuMZ2nhMzx --- CHANGELOG.md | 19 +++ CLAUDE.md | 2 + README.md | 20 ++- docs/02-guides/scripting-guide.md | 168 +++++++++++++++++++++++-- docs/03-reference/command-reference.md | 153 ++++++++++++++++++++-- 5 files changed, 339 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8719905..82fc70a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.18.0] - 2026-06-26 + +### Added +- **`exec` command — inject credentials as environment variables** (#98) — `pass-cli exec` runs a child command with stored credentials passed only through its environment, so the secret never touches a file, the clipboard, or shell history. Supports an explicit, repeatable `--set ENV_NAME=service[:field]` mapping and a convenience form (`pass-cli exec -- `) that derives the env name from the service (uppercased, non-alphanumeric → `_`, e.g. `openai-api` → `OPENAI_API`). The `-f/--field` flag selects the field for all mappings (default `password`); a per-mapping `:field` suffix overrides it, allowing two fields of one entry to be injected as separate variables. Everything after `--` is the child argv, and the child's exit code is propagated unchanged. `exec` is read-only: it records no usage and triggers no sync push, making it safe on a hot path. + +### Changed +- **`list` is now safe-by-default** (#95, #97) — the default table hides the Username column, because the "username" field can hold sensitive values (card, account, or routing numbers stored as a username). New `--show-usernames` re-adds the column, and new `-q/--quiet` is a shorthand for `--format simple` (bare service names, one per line) that takes precedence over `--format`. `--format json` is unchanged and still emits full metadata including usernames as an explicit, structured opt-in. +- **Sync: content-hash change detection** (#102) — push change-detection now uses a name-encoded zero-byte marker (`vault.enc..synchash`) written next to the vault on each push, replacing the old modtime+size heuristic; older vaults without a marker fall back to the previous behavior. +- **Sync: lower unlock latency by overlapping the pull** (#103) — the pre-unlock remote pull now runs concurrently with the master-password prompt (Tier 1, #109) and with PBKDF2-SHA256 key derivation on keychain unlock (Tier 2, #110). Internal only; no flag or UX change. +- **Sync: cut sync-related startup latency** (#104) — removed the dead `--hash` flag and the "Syncing… done" feedback line. The global `--offline` flag is unaffected and still works. + +### Fixed +- **Test: resolve `captureStdout` redeclaration across `cmd` test files** (#99) + +### Infrastructure +- **Tracked `mise.toml`** (#100) — `mise.toml` is now committed so the documented `mise run …` tasks work on a fresh clone (previously gitignored). +- **CI runs on all PRs** (#105) — required status checks now report for non-code-change PRs as well. +- **pass-cli agent skill** (#101) — added an agent skill under `.claude/skills` and tracked the directory. + ## [0.17.2] - 2026-01-31 ### Changed diff --git a/CLAUDE.md b/CLAUDE.md index 30975f09..e290fda3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,9 @@ mise run gh # GitHub CLI operations - Vault is a single encrypted file — all pushes/pulls transfer one file regardless of change count - `SmartPush` hashes local file, skips network if hash matches `LastPushHash` in `.sync-state` +- Change detection is content-hash based (#102): each push writes a zero-byte marker `vault.enc..synchash` next to the vault; `SmartPull` reads the hash from the marker name in the `rclone lsjson` listing it already fetches (no extra round trip), replacing the old modtime+size heuristic — markerless older vaults fall back to it - `SmartPull` runs before unlock, `SmartPush` runs after command completes (synchronous, blocks prompt) +- The pre-unlock pull overlaps unlock work (#103): the master-password prompt (Tier 1) and, on keychain unlock, the PBKDF2 key derivation (Tier 2) run concurrently with the pull, which joins before decrypt — see `cmd/helpers.go` - `RecordFieldAccess` (called by `get`) writes usage timestamps, changing vault hash and triggering push - Two network round-trips per push: `rclone sync` + `rclone lsjson` - Async push risks: silent failures, false conflicts (kill between push and SaveState), partial uploads on some backends diff --git a/README.md b/README.md index b82006af..e745e480 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,16 @@ pass-cli add github # Retrieve a credential pass-cli get github -# List all credentials +# List all credentials (usernames hidden by default; -q for bare names) pass-cli list +# Run a command with a credential injected as an environment variable +# (the secret never touches a file, the clipboard, or your shell history) +pass-cli exec --set GITHUB_TOKEN=github -- gh repo list + +# Convenience form: derive the env name from the service (github -> GITHUB) +pass-cli exec github -- gh repo list + # Use in scripts (quiet mode) export API_KEY=$(pass-cli get myservice --quiet --field password) ``` @@ -109,9 +116,18 @@ pass-cli get github # Get credential (script-friendly) pass-cli get github --quiet --field password -# List all credentials +# Run a command with a credential injected as an env var (never hits disk/clipboard) +pass-cli exec --set GITHUB_TOKEN=github -- gh repo list + +# List all credentials (table hides usernames by default) pass-cli list +# List bare service names, one per line +pass-cli list -q + +# List including the username column (may reveal sensitive values) +pass-cli list --show-usernames + # Update credential pass-cli update github --username newuser@example.com diff --git a/docs/02-guides/scripting-guide.md b/docs/02-guides/scripting-guide.md index adb0d0a9..9133e2c7 100644 --- a/docs/02-guides/scripting-guide.md +++ b/docs/02-guides/scripting-guide.md @@ -35,17 +35,119 @@ pass-cli get github --field username --quiet ### Simple Mode (List Only) -Service names only, one per line. +Service names only, one per line. The `-q/--quiet` flag is a shorthand for +`--format simple` and takes precedence over `--format`. ```bash -pass-cli list --format simple +pass-cli list -q # github # aws-prod # database + +# Equivalent, explicit form +pass-cli list --format simple +``` + +**Username safety:** the default `list` table **hides the Username column**, because the +"username" field can hold sensitive values (card, account, or routing numbers stored as a +username). Pass `--show-usernames` to add the column back: + +```bash +pass-cli list --show-usernames +``` + +Note that `--format json` is an explicit, structured opt-in and still emits the full +metadata, **including usernames**: + +```bash +pass-cli list --format json +``` + +## Inject Credentials with `exec` (Recommended) + +When the goal is to hand a credential to a child command, prefer `pass-cli exec`. +It injects the secret as an **environment variable** in the child process only - the +value never touches a file, the clipboard, or your shell history, and `pass-cli` +writes nothing of its own to stdout. This avoids the leak surface of command +substitution (covered [below](#capturing-values-with-command-substitution-fallback)), +where any layer that echoes the command - `set -x`, CI job logs, file-watching tools - +can capture the value. + +`exec` is read-only: it does not record usage or trigger a sync push, so it is safe to +call repeatedly on a hot path (for example, inside an agent or a tight CI loop). + +**Explicit mapping (`--set ENV_NAME=service`, repeatable):** + +```bash +# Inject the password of "github" as GITHUB_TOKEN, then run gh +pass-cli exec --set GITHUB_TOKEN=github -- gh repo list +``` + +Everything after `--` is the command to run. There must be a `--` followed by a +command, or `exec` errors. + +**Convenience form (derive the env name from the service):** + +```bash +# Service name -> env name: uppercased, every non-alphanumeric char -> '_' +# "openai-api" sets OPENAI_API to the password field. +pass-cli exec openai-api -- python train.py +``` + +Do not combine a positional `` with `--set` - pick one form per command. + +**Multiple credentials at once:** + +```bash +pass-cli exec \ + --set AWS_ACCESS_KEY_ID=aws-id \ + --set AWS_SECRET_ACCESS_KEY=aws-secret \ + -- aws s3 ls +``` + +**Selecting a field.** `-f/--field` (default `password`) selects the field for every +mapping; valid fields are `username`, `password`, `category`, `url`, `notes`, `service`. + +```bash +# Inject the username field instead of the password +pass-cli exec --set DB_USER=postgres --field username -- ./run-migration.sh +``` + +**Two fields of one entry as separate variables.** A per-mapping `:field` suffix +overrides `--field`, so you can inject both a username and a password from the same +credential: + +```bash +pass-cli exec \ + --set DB_USER=postgres:username \ + --set DB_PASSWORD=postgres:password \ + -- ./run-migration.sh ``` +**Exit-code propagation.** The child's exit code is propagated unchanged, so error +handling works exactly as if you had run the command directly: + +```bash +pass-cli exec --set GITHUB_TOKEN=github -- gh repo list +echo "exit code: $?" # the exit code of gh, untouched by pass-cli +``` + +**Security note (honest scope).** The injected value lives in the child process's +environment. On Linux it is readable via `/proc//environ` by the same user and is +inherited by descendant processes. This is the same model as `op run` and +`aws-vault exec` - far safer than files, clipboards, or shell history, but it is **not** +process isolation. + ## Script Integration +### Capturing Values with Command Substitution (Fallback) + +When you need the secret *inside your own script* - not just to launch a child command - +capture it with command substitution. This pattern works, but the value is exposed to +anything that echoes the command (`set -x`, CI job logs, file-watching tools), so prefer +[`pass-cli exec`](#inject-credentials-with-exec-recommended) whenever you are simply +handing the credential to another command. + ### Bash/Zsh Examples **Export to environment variable:** @@ -65,6 +167,9 @@ mysql -u "$(pass-cli get testservice -f username -q)" \ mydb ``` +> **Tip:** To run `mysql` without the password landing in your shell, use +> `exec` instead: `pass-cli exec --set MYSQL_PWD=testservice -- mysql -u mydb`. + **Conditional execution:** ```bash @@ -82,7 +187,7 @@ fi ```bash # Process all credentials -for service in $(pass-cli list --format simple); do +for service in $(pass-cli list -q); do echo "Processing $service..." username=$(pass-cli get "$service" --field username --quiet) echo " Username: $username" @@ -147,13 +252,22 @@ username = result.stdout.strip() ### Makefile Examples +Lead with `exec` so the secrets are not interpolated into recipe lines (which `make` +echoes by default unless prefixed with `@`): + ```makefile .PHONY: deploy deploy: - @export AWS_KEY=$$(pass-cli get aws --quiet --field username); \ - export AWS_SECRET=$$(pass-cli get aws --quiet); \ - ./deploy.sh + @pass-cli exec \ + --set AWS_KEY=aws:username \ + --set AWS_SECRET=aws:password \ + -- ./deploy.sh +``` + +Command substitution is still the right tool when you must *build* a value (like a DSN) +inside the recipe; keep it under `@` so the line is not echoed: +```makefile .PHONY: test-db test-db: @DB_URL="postgres://$$(pass-cli get testdb -f username -q):$$(pass-cli get testdb -q)@localhost/testdb" \ @@ -196,10 +310,11 @@ pass-cli get github ### Scripting -1. **Always use `--quiet`** - Clean output for variables -2. **Check exit codes** - Handle errors properly -3. **Use `--field`** - Extract exactly what you need -4. **Redirect stderr** - Control error output +1. **Prefer `exec` to hand a credential to a command** - Keeps the value out of files, history, and logs +2. **Use `--quiet` when you must capture a value** - Clean output for variables +3. **Check exit codes** - Handle errors properly (`exec` propagates the child's code unchanged) +4. **Use `--field`** - Extract exactly what you need +5. **Redirect stderr** - Control error output ### Examples @@ -222,6 +337,21 @@ pass-cli add service --password mySecretPassword ### CI/CD Pipeline +Inject the deployment credentials straight into the deploy command with `exec`, so the +values never appear in the job log (unlike `export VAR=$(...)`, which CI logging or +`set -x` can capture). `deploy.sh` reads `$DEPLOY_KEY` and `$DB_PASSWORD` from its +environment, and its exit code propagates unchanged: + +```bash +pass-cli exec \ + --set DEPLOY_KEY=production \ + --set DB_PASSWORD=prod-db \ + -- ./deploy.sh +``` + +
+Command-substitution fallback (exposes values to job logs / set -x) + ```bash # Retrieve deployment credentials export DEPLOY_KEY=$(pass-cli get production --quiet) @@ -231,8 +361,24 @@ export DB_PASSWORD=$(pass-cli get prod-db --quiet) ./deploy.sh ``` +
+ ### Local Development +Pull several fields of one entry (`dev-db`) into separate variables with per-mapping +`:field` overrides, and launch the dev server in one step: + +```bash +pass-cli exec \ + --set DB_HOST=dev-db:url \ + --set DB_USER=dev-db:username \ + --set DB_PASS=dev-db:password \ + -- npm run dev +``` + +
+Command-substitution fallback (exposes values to shell tracing) + ```bash # Set up environment from credentials export DB_HOST=$(pass-cli get dev-db --field url --quiet) @@ -243,6 +389,8 @@ export DB_PASS=$(pass-cli get dev-db --quiet) npm run dev ``` +
+ ### Credential Rotation ```bash diff --git a/docs/03-reference/command-reference.md b/docs/03-reference/command-reference.md index 42bb3d6b..f15011c1 100644 --- a/docs/03-reference/command-reference.md +++ b/docs/03-reference/command-reference.md @@ -391,6 +391,109 @@ $ pass-cli get github --totp --- +### exec - Run a Command with Injected Credentials + +Run a child command with stored credentials injected as environment variables. + +#### Synopsis + +```bash +pass-cli exec [] -- [args...] +``` + +#### Description + +`exec` runs a child command with stored credentials injected as **environment variables**. The secret value is passed only through the child process's environment — it never touches a file, the clipboard, or your shell history. + +Everything after the `--` separator is the command to run. There **must** be a `--` and a command after it. pass-cli writes nothing of its own to stdout, and the child's exit code is **propagated unchanged**. + +`exec` is read-only: it does **not** record usage (no usage tracking) and does **not** trigger a sync push, so repeated invocations on a hot path (or inside an agent) don't mutate the vault on every run. + +#### Mapping Forms + +There are two ways to map credentials to environment variables: + +**Explicit mapping (`--set`, repeatable):** + +```bash +# --set ENV_NAME=service[:field] +pass-cli exec --set GITHUB_TOKEN=github -- gh repo list +``` + +**Convenience form (derive the env name from the service):** + +The env name is derived from the service name by uppercasing it and replacing every non-alphanumeric character with `_`. For example, `openai-api` becomes `OPENAI_API`. + +```bash +pass-cli exec openai-api -- python train.py # sets OPENAI_API +``` + +The positional `` form and `--set` are mutually exclusive — use one or the other. + +#### Field Selection + +The `-f`/`--field` flag selects which field to inject (default: `password`) and applies to **all** mappings. Valid fields: + +- `username` +- `password` +- `category` +- `url` +- `notes` +- `service` + +A single mapping can override the global field with a `:field` suffix (`--set ENV=service:field`). The per-mapping `:field` always wins over `--field`. This is how you inject two fields of the **same** entry as separate variables (for example, a database username and password): + +```bash +pass-cli exec \ + --set DB_USER=postgres:username \ + --set DB_PASSWORD=postgres:password \ + -- ./run.sh +``` + +#### Flags + +| Flag | Short | Type | Description | +|------|-------|------|-------------| +| `--set` | | string (repeatable) | Map an environment variable to a credential: `ENV_NAME=service[:field]` | +| `--field` | `-f` | string | Field to inject for all mappings (default: `password`); per-mapping `:field` overrides it | + +#### Examples + +```bash +# Inject a password as GITHUB_TOKEN and run gh +pass-cli exec --set GITHUB_TOKEN=github -- gh repo list + +# Multiple credentials at once +pass-cli exec \ + --set AWS_ACCESS_KEY_ID=aws-id \ + --set AWS_SECRET_ACCESS_KEY=aws-secret \ + -- aws s3 ls + +# Inject a non-password field for all mappings +pass-cli exec --set DB_USER=postgres --field username -- ./run-migration.sh + +# Inject two fields of one entry as separate variables (per-mapping override) +pass-cli exec \ + --set DB_USER=postgres:username \ + --set DB_PASSWORD=postgres:password \ + -- ./run-migration.sh + +# Convenience form: service name -> env name (openai-api -> OPENAI_API) +pass-cli exec openai-api -- python train.py +``` + +#### Security Note + +The injected value lives in the **child process's environment**. On Linux it is readable via `/proc//environ` by the same user, and it is inherited by descendant processes. This is the same model as `op run` and `aws-vault exec` — far safer than files, clipboards, or shell history, but it is **not** process isolation. + +#### Notes + +- A `--` separator and a command after it are **required**. +- The child's exit code is propagated unchanged; pass-cli writes nothing to stdout itself. +- **Sync**: Read-only — does not record usage and does not trigger a sync push (even with sync enabled). + +--- + ### list - List Credentials List all credentials in the vault. @@ -406,19 +509,33 @@ pass-cli list [flags] | Flag | Type | Description | |------|------|-------------| | `--format`, `-f` | string | Output format: table, json, simple (default: table) | +| `--show-usernames` | bool | Include the Username column in the table (hidden by default) | +| `--quiet`, `-q` | bool | Print bare service names only (alias for `--format simple`; takes precedence over `--format`) | | `--unused` | bool | Show only unused credentials | | `--days` | int | Days threshold for unused (default: 30) | | `--by-project` | bool | Group credentials by git repository | | `--location` | string | Filter credentials by directory path | | `--recursive` | bool | Include subdirectories with --location | +#### Username Safety (Default) + +By default, the table output **hides the Username column**. The `username` field can hold sensitive values (card, account, or routing numbers stored as a username), so a broad `list` should not dump them. Pass `--show-usernames` to add the column back (it is omitted entirely, not masked). + +The `--format json` output is unchanged: it is an explicit, structured opt-in and still emits the full metadata, **including usernames**. + #### Examples ```bash -# List all credentials (table format) +# List all credentials (table format, usernames hidden) pass-cli list -# List as JSON +# Include the username column (may reveal sensitive values) +pass-cli list --show-usernames + +# List bare service names, one per line +pass-cli list -q + +# List as JSON (includes usernames) pass-cli list --format json # Simple list (service names only) @@ -448,18 +565,29 @@ pass-cli list --location ~/work --by-project --recursive #### Output Examples -**Table format (default):** +**Table format (default — usernames hidden):** +```text ++----------+-----------+---------------------+---------------------+ +| SERVICE | USAGE | LAST USED | CREATED | ++----------+-----------+---------------------+---------------------+ +| github | 12 (2 loc)| 2 hours ago | 3 months ago | +| aws-prod | 5 | 2 days ago | 6 months ago | +| database | Never | Never | 1 month ago | ++----------+-----------+---------------------+---------------------+ +``` + +**Table format with `--show-usernames`:** ```text -+----------+----------------------+---------------------+ -| SERVICE | USERNAME | LAST ACCESSED | -+----------+----------------------+---------------------+ -| github | user@example.com | 2025-01-20 14:22 | -| aws-prod | admin@company.com | 2025-01-18 09:15 | -| database | dbuser | 2025-01-15 16:30 | -+----------+----------------------+---------------------+ ++----------+----------------------+-----------+---------------------+---------------------+ +| SERVICE | USERNAME | USAGE | LAST USED | CREATED | ++----------+----------------------+-----------+---------------------+---------------------+ +| github | user@example.com | 12 (2 loc)| 2 hours ago | 3 months ago | +| aws-prod | admin@company.com | 5 | 2 days ago | 6 months ago | +| database | dbuser | Never | Never | 1 month ago | ++----------+----------------------+-----------+---------------------+---------------------+ ``` -**Simple format:** +**Simple format (also `-q`/`--quiet`):** ```text github aws-prod @@ -469,6 +597,9 @@ database #### Notes - Passwords are never shown in list output +- Usernames are hidden in the table by default; use `--show-usernames` to include them (the `username` field can hold sensitive values) +- `--format json` still includes usernames (explicit structured opt-in) +- `-q`/`--quiet` is shorthand for `--format simple` and takes precedence over `--format` - Table format is best for human viewing - JSON format is best for parsing - Simple format is best for shell scripts