diff --git a/CLAUDE.md b/CLAUDE.md index 8a522e9..461b633 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,9 +25,9 @@ Single bash script (`secrets`) with subcommands: init, push, pull, list, rm, rek - Encryption: `age` with key files (not passphrases — age passphrases are non-scriptable) - Storage: Private git repo at `~/.secrets/` -- Convention: Globs `.env` and `.env.*` (not `.envrc`, `.environment-*`) +- Convention: Tracks `.env`, `.env.*`, and `.dev.vars` (not `.envrc`, `.environment-*`) - Workspaces: `--workspaces` flag reads `package.json` workspaces, requires `jq` -- Safety: Pre-commit hook rejects plaintext `.env` files +- Safety: Pre-commit hook rejects plaintext secret files ## Project Structure diff --git a/README.md b/README.md index 04b80ce..1fd37bd 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,257 @@ # secrets -Sync `.env` files between machines without storing them in git. Encrypts with [age](https://github.com/FiloSottile/age), stores in a private repo. +A command-line tool for sharing secret files (API keys, database passwords, tokens) between your machines and teammates — without ever putting them in your project's git history. -## Install +## The problem + +Most projects have files like `.env`, `.env.staging`, or `.dev.vars` that contain sensitive credentials. These files should never be committed to your project's git repository because: + +- Anyone with access to the repo can see them (even if you delete them later — git keeps history forever) +- Automated tools, CI pipelines, and compromised dependencies can read plaintext files from your project directory +- There's no safe built-in way to share these files between your laptop, your desktop, or a teammate's machine + +People end up sharing secrets over Slack, email, or sticky notes. When a key changes, someone forgets to update, and things break. + +## What this tool does + +`secrets` encrypts your secret files and stores them in a separate, private git repository. Only someone with the encryption key can read them. + +``` +Your project (e.g. ~/myapp/) Your secrets store (~/.secrets/) +┌──────────────────────────┐ ┌──────────────────────────┐ +│ .env (plaintext)│──encrypt─▶│ myapp/.env.age (encrypted)│──sync──▶ GitHub (private) +│ .env.staging (plaintext)│ │ myapp/.env.staging.age │ +│ .dev.vars (plaintext)│ │ key.txt (never uploaded) │ +└──────────────────────────┘ └──────────────────────────┘ +``` + +- **Encrypted at rest** — files are encrypted with [age](https://github.com/FiloSottile/age), a modern encryption tool. Without the key, the files are unreadable. +- **Synced via git** — the encrypted files are stored in a private git repository that syncs between machines. You never interact with this repo directly — `secrets push` and `secrets pull` handle it. +- **Minimal exposure** — `secrets run` keeps plaintext files on disk only while your command is running, then deletes them automatically. + +### What files are tracked + +| Pattern | Example | Source | +|---------|---------|--------| +| `.env` | `SECRET_KEY=abc123` | Standard environment file | +| `.env.*` | `.env.staging`, `.env.production` | Environment-specific variants | +| `.dev.vars` | `CF_API_TOKEN=xyz` | Cloudflare Wrangler local secrets | + +Files like `.envrc` (direnv) and `.environment-*` are intentionally **not** tracked. + +## Prerequisites + +- **macOS** (uses Homebrew for installation) +- **git** (already installed on most Macs — type `git --version` to check) +- **age** (the encryption tool — installed in step 1 below) + +## Setup + +### First machine (one-time setup) ```bash -# 1. Install age (encryption tool) +# 1. Install the encryption tool brew install age -# 2. Clone this repo (the tool's source code) +# 2. Download the secrets tool +# Replace with your GitHub username or org git clone git@github.com:/secrets.git ~/dev/secrets -# 3. Add it to your PATH (e.g., in ~/.zshrc) +# 3. Make the 'secrets' command available everywhere +# Add this line to your shell config file (~/.zshrc on Mac): export PATH="$HOME/dev/secrets:$PATH" +# Then restart your terminal, or run: +source ~/.zshrc -# 4. Initialize the encrypted secrets store (separate repo) +# 4. Initialize your encrypted secrets store +# This creates a folder at ~/.secrets/ with your encryption key secrets init -# 5. Create a PRIVATE repo on GitHub for your encrypted secrets, then: +# 5. Create a PRIVATE repository on GitHub to store your encrypted secrets +# Go to github.com/new, name it something like 'my-secrets', and make sure +# "Private" is selected. Then connect it: cd ~/.secrets git remote add origin git@github.com:/my-secrets.git git push -u origin main - -# 6. Copy the key file to your other machine (one-time) -scp ~/.secrets/key.txt :~/.secrets/key.txt ``` -This repo (`~/dev/secrets`) is the **tool** — the CLI script, tests, and docs. -`~/.secrets/` is the **encrypted secrets store** — a separate private git repo -where your `.env.age` files live. They are two different repos. +> **Important:** Step 5 creates a *separate* private repo for your encrypted secrets. This is different from the `secrets` tool repo you cloned in step 2. The tool repo can be public — it contains no secrets. The `~/.secrets/` repo must be private. + +### Additional machines + +On each new machine (your desktop, a teammate's laptop, etc.): + +```bash +# 1. Install prerequisites and the tool (same as steps 1-3 above) +brew install age +git clone git@github.com:/secrets.git ~/dev/secrets +export PATH="$HOME/dev/secrets:$PATH" # add to ~/.zshrc + +# 2. Clone the encrypted secrets repo +git clone git@github.com:/my-secrets.git ~/.secrets + +# 3. Copy the encryption key from your first machine +# This is the only step that requires direct machine-to-machine transfer. +# Choose one method: +# +# Option A: AirDrop (Mac to Mac) +# On your first machine, right-click ~/.secrets/key.txt → Share → AirDrop +# Save it to ~/.secrets/key.txt on the new machine +# +# Option B: Secure copy over SSH +# scp first-machine:~/.secrets/key.txt ~/.secrets/key.txt +# +# Option C: USB drive +# Copy key.txt to a USB drive, transfer it, delete from USB after + +# 4. Pull your secrets into any project +cd ~/myapp +secrets pull +``` + +> **The key file (`~/.secrets/key.txt`) is the only thing that needs to be transferred manually.** It never leaves your machines — it's excluded from git, never uploaded, never transmitted over the internet. Anyone with this file can decrypt all your secrets, so treat it like a password. + +### Sharing with teammates + +To share secrets with a teammate, they need: + +1. Access to your private `my-secrets` GitHub repo (add them as a collaborator) +2. A copy of `key.txt` (send it to them directly — AirDrop, USB, or in-person) + +Everyone on the team uses the same key. When anyone runs `secrets push`, the encrypted files are updated and everyone else can `secrets pull` to get the latest version. ## Usage +### Daily workflow + ```bash -secrets init # Create ~/.secrets repo + generate age key -secrets push [project] # Encrypt .env* files and push -secrets pull [project] # Pull and decrypt .env* files into current dir -secrets push -w|--workspaces # Push .env* from all package.json workspaces -secrets pull -w|--workspaces # Pull .env* into all package.json workspaces -secrets list # Show all projects -secrets rm # Remove a project's secrets -secrets rekey # Re-encrypt everything with a new key +# Start of your work session — pull the latest secrets into your project +cd ~/myapp +secrets pull + +# ... code, test, deploy ... + +# If you changed any secret files, push the updates +secrets push + +# End of session — remove plaintext secrets from disk (optional but recommended) +secrets clear ``` -If `[project]` is omitted, it's derived from the current directory's git remote or name. +### Command reference + +| Command | What it does | +|---------|-------------| +| `secrets init` | Create the `~/.secrets/` repo and generate an encryption key | +| `secrets push` | Encrypt secret files in the current directory and upload them | +| `secrets pull` | Download and decrypt secret files into the current directory | +| `secrets clear` | Delete plaintext secret files from the current directory | +| `secrets run ` | Pull secrets, run a command, then clear secrets when it exits | +| `secrets list` | Show all projects that have stored secrets | +| `secrets rm ` | Delete a project's secrets from the store | +| `secrets rekey` | Generate a new encryption key and re-encrypt everything | + +### Automatic project detection + +When you run `secrets push` or `secrets pull` without specifying a project name, the tool figures out which project you're in by: + +1. Checking the current directory's git remote (e.g., `origin` → `github.com/you/myapp.git` → `myapp`) +2. Falling back to the directory name (e.g., `/Users/you/myapp` → `myapp`) + +You can also specify a name explicitly: `secrets push myapp`. + +### Minimizing plaintext exposure + +Every second that plaintext secret files sit on disk is a window for a compromised tool or dependency to read them. `secrets run` shrinks that window to only while your command is running: + +```bash +secrets run npm start # .env exists only while dev server is up +secrets run wrangler deploy # .dev.vars exists only during deploy +``` + +When the command exits — whether normally, from an error, or from Ctrl-C — the plaintext files are automatically deleted. + +This works in `package.json` scripts too, so your whole team gets the protection automatically: + +```json +{ + "scripts": { + "dev": "secrets run react-router dev --port 5173", + "deploy": "secrets run wrangler deploy" + } +} +``` + +Now `npm run dev` pulls secrets, starts the dev server, and clears secrets when you stop it. ### Monorepo support -For monorepos with `package.json` workspaces, use `--workspaces` (`-w`) from the repo root: +For projects with multiple packages (monorepos using `package.json` workspaces), add the `-w` flag to operate on all workspaces at once: ```bash -cd ~/myapp # has package.json with "workspaces": ["apps/*", "packages/*"] -secrets push -w # encrypts .env* from root + each workspace -secrets pull -w # decrypts into root + each workspace directory +cd ~/myapp # has package.json with "workspaces": ["apps/*", "packages/*"] +secrets push -w # encrypts secrets from root + each workspace +secrets pull -w # decrypts into root + each workspace directory +secrets clear -w # clears secrets from root + each workspace +secrets run -w turbo dev # pull all, run command, clear all on exit ``` -Secrets are stored as `//` in `~/.secrets/`: +Inside `~/.secrets/`, workspace secrets are organized by path: ``` ~/.secrets/ myapp/ - .env.age # root - apps/web/.env.staging.age # workspace - apps/api/.env.age # workspace + .env.age # root project secrets + apps/web/.env.staging.age # web app workspace + apps/api/.env.age # api workspace ``` Requires `jq` (`brew install jq`). -## How it works +## Safety features -``` -Your project dir ~/.secrets/ (private git repo) GitHub (private) -┌──────────────┐ ┌────────────────────┐ ┌──────────┐ -│ .env.staging │──age──▶ │ proj/.env.staging │──git push──▶ │ encrypted│ -│ .env.prod │ encrypt │ .age │ │ .age │ -└──────────────┘ │ key.txt (gitignored)│ │ files │ - └────────────────────┘ └──────────┘ -``` +- **`secrets run` auto-clears** — plaintext files are deleted when the command exits, errors, or is interrupted with Ctrl-C +- **Pre-commit hook** — a git hook in `~/.secrets/` prevents accidentally committing plaintext secret files to the encrypted store +- **Key is never uploaded** — `key.txt` is gitignored and never leaves your machine via git +- **Encryption is file-level** — each secret file is independently encrypted. A corrupted file doesn't affect others. -1. `secrets init` generates an age key pair at `~/.secrets/key.txt` -2. `secrets push` encrypts `.env` and `.env.*` files, commits to the secrets repo, pushes -3. On your other machine: `secrets pull` fetches and decrypts into the current directory +## Key rotation -The key file must be copied to each machine once (AirDrop, scp, USB). - -## Safety - -- A pre-commit hook in `~/.secrets/` rejects any plaintext `.env` file -- `.gitignore` blocks `key.txt` and plaintext env files from being committed -- Only `.env` and `.env.*` files are matched (not `.envrc`, `.environment-*`, etc.) - -## Testing +If you suspect your key has been compromised, or a teammate leaves the team: ```bash -brew install bats-core -bats test/secrets.bats # 25 tests +secrets rekey +``` + +This generates a new key and re-encrypts all secrets. After rekeying: + +1. Copy the new `~/.secrets/key.txt` to every machine and teammate +2. Old encrypted files remain in git history (encrypted with the old key, which should be discarded) + +For complete rotation with no historical exposure, create a fresh `~/.secrets/` repo. + +## Environment variables + +| Variable | Default | Purpose | +|----------|---------|---------| +| `SECRETS_DIR` | `~/.secrets` | Override the secrets store location | + +## Troubleshooting + +**"Key file not found"** — You need `~/.secrets/key.txt`. Either run `secrets init` (first machine) or copy it from a machine that has it. + +**"Not initialized"** — Run `secrets init` to create the `~/.secrets/` directory. + +**"No secret files found"** — You're in a directory that doesn't have `.env`, `.env.*`, or `.dev.vars` files. Make sure you're in the right project directory. + +**"Project not found"** — The project name doesn't match anything in `~/.secrets/`. Run `secrets list` to see what's stored. The name is usually derived from your directory name or git remote. + +**"Fast-forward pull failed"** — Someone else pushed secrets while you had local changes. Run `secrets pull` first, then retry your push. + +## Development + +```bash +# Run the test suite (37 tests) +brew install bats-core +bats test/secrets.bats ``` diff --git a/hooks/pre-commit b/hooks/pre-commit index b8ba9d9..dc6570a 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -3,9 +3,9 @@ # Rejects staged files matching .env patterns without .age extension. # This is a safety net, not a security boundary (--no-verify bypasses it). -BLOCKED=$(git diff --cached --name-only | grep -E '\.env' | grep -v '\.age$' || true) +BLOCKED=$(git diff --cached --name-only | grep -E '\.(env|dev\.vars)' | grep -v '\.age$' || true) if [ -n "$BLOCKED" ]; then - echo "ERROR: Plaintext env files staged for commit:" + echo "ERROR: Plaintext secret files staged for commit:" echo "$BLOCKED" echo "Only .age (encrypted) files should be committed." exit 1 diff --git a/secrets b/secrets index 5046c86..0b24ce2 100755 --- a/secrets +++ b/secrets @@ -46,12 +46,13 @@ derive_project_name() { basename "$PWD" } -# Collect .env and .env.* files from a directory (excluding .envrc, .environment-*) +# Collect secret files from a directory: +# .env, .env.*, .dev.vars (excluding .envrc, .environment-*) # Sets the COLLECTED_FILES array. Returns 1 if no files found. collect_env_files() { local dir="$1" COLLECTED_FILES=() - for f in "$dir"/.env "$dir"/.env.*; do + for f in "$dir"/.env "$dir"/.env.* "$dir"/.dev.vars; do [ -f "$f" ] || continue local basename_f basename_f=$(basename "$f") @@ -97,9 +98,9 @@ install_hook() { # Inline hook if template not found (e.g. secrets installed standalone) cat > "$hook_dst" << 'HOOKEOF' #!/usr/bin/env bash -BLOCKED=$(git diff --cached --name-only | grep -E '\.env' | grep -v '\.age$' || true) +BLOCKED=$(git diff --cached --name-only | grep -E '\.(env|dev\.vars)' | grep -v '\.age$' || true) if [ -n "$BLOCKED" ]; then - echo "ERROR: Plaintext env files staged for commit:" + echo "ERROR: Plaintext secret files staged for commit:" echo "$BLOCKED" echo "Only .age (encrypted) files should be committed." exit 1 @@ -132,13 +133,15 @@ cmd_init() { # Never commit the private key key.txt -# Block plaintext env files +# Block plaintext secret files **/.env **/.env.* +**/.dev.vars -# Allow encrypted env files +# Allow encrypted files !**/.env.age !**/.env.*.age +!**/.dev.vars.age EOF # Install pre-commit hook @@ -220,7 +223,7 @@ cmd_push() { pubkey=$(get_pubkey) if ! push_dir_to_project "$PWD" "$project" "$pubkey"; then - die "No .env or .env.* files found in $PWD" + die "No secret files (.env, .env.*, .dev.vars) found in $PWD" fi commit_and_push_secrets "update $project" @@ -260,7 +263,7 @@ cmd_push_workspaces() { done <<< "$workspaces" if [ "$total" -eq 0 ]; then - die "No .env files found in any workspace" + die "No secret files found in any workspace" fi commit_and_push_secrets "update $monorepo_name workspaces" @@ -513,27 +516,114 @@ cmd_rekey() { echo "For full rotation, create a fresh repo." } +cmd_clear() { + local dir="$PWD" + if ! collect_env_files "$dir"; then + info "No secret files to clear in $dir" + return + fi + + local count=0 + for f in "${COLLECTED_FILES[@]}"; do + rm "$f" + count=$((count + 1)) + done + info "Cleared $count secret file(s) from $dir" +} + +cmd_clear_workspaces() { + check_cmd jq + + local root="$PWD" + local total=0 + + # Clear root + if collect_env_files "$root"; then + for f in "${COLLECTED_FILES[@]}"; do + rm "$f" + total=$((total + 1)) + done + fi + + # Clear each workspace + local workspaces + workspaces=$(get_workspaces "$root") + while IFS= read -r ws; do + [ -n "$ws" ] || continue + local ws_dir="$root/$ws" + if collect_env_files "$ws_dir"; then + for f in "${COLLECTED_FILES[@]}"; do + rm "$f" + total=$((total + 1)) + done + fi + done <<< "$workspaces" + + info "Cleared $total secret file(s) from workspace" +} + +cmd_run() { + local workspace_mode=false + local project="" + + # Parse flags before the command + while [ $# -gt 0 ]; do + case "$1" in + -w|--workspaces) workspace_mode=true; shift ;; + --) shift; break ;; + -*) die "Unknown flag: $1. Usage: secrets run [-w] [--] " ;; + *) break ;; + esac + done + + [ $# -gt 0 ] || die "Usage: secrets run [-w] [--] " + + # Pull secrets + if [ "$workspace_mode" = true ]; then + cmd_pull_workspaces + else + cmd_pull "$project" + fi + + # Set trap to clear secrets on exit (normal, error, interrupt, terminate) + if [ "$workspace_mode" = true ]; then + trap 'cmd_clear_workspaces' EXIT + else + trap 'cmd_clear' EXIT + fi + + # Execute the command, capturing exit code (don't let set -e kill us) + local rc=0 + "$@" || rc=$? + exit "$rc" +} + cmd_help() { cat << 'EOF' -secrets — encrypted env file sync between machines +secrets — encrypted secret file sync between machines Usage: secrets init Initialize the secrets repo and generate an age key - secrets push [project] Encrypt .env* files and push to the secrets repo - secrets push -w|--workspaces Push .env* from all workspaces in package.json - secrets pull [project] Pull and decrypt .env* files into current directory - secrets pull -w|--workspaces Pull .env* into all workspaces from package.json + secrets push [project] Encrypt secret files and push to the secrets repo + secrets push -w|--workspaces Push secrets from all workspaces in package.json + secrets pull [project] Pull and decrypt secret files into current directory + secrets pull -w|--workspaces Pull secrets into all workspaces from package.json + secrets clear Remove plaintext secret files from current directory + secrets clear -w|--workspaces Clear secrets from all workspaces in package.json + secrets run [-w] Pull secrets, run command, clear secrets on exit secrets list List all projects and their secret files secrets rm Remove a project's secrets from the repo secrets rekey Re-encrypt all secrets with a new key +Tracked files: .env, .env.*, .dev.vars + If [project] is omitted, it is derived from the current directory's git remote (if available) or the directory name. Workspaces: With -w/--workspaces, reads package.json "workspaces" field to find - workspace directories. Each workspace's .env* files are stored under - // in the secrets repo. Root .env* files + workspace directories. Each workspace's secret files are stored under + // in the secrets repo. Root secret files are stored under / directly. Requires jq. Environment: @@ -559,6 +649,17 @@ case "${1:-help}" in cmd_pull "${2:-}" fi ;; + clear) + if [ "${2:-}" = "-w" ] || [ "${2:-}" = "--workspaces" ]; then + cmd_clear_workspaces + else + cmd_clear + fi + ;; + run) + shift + cmd_run "$@" + ;; list) cmd_list ;; rm) cmd_rm "${2:-}" ;; rekey) cmd_rekey ;; diff --git a/test/secrets.bats b/test/secrets.bats index 1734efe..504c374 100644 --- a/test/secrets.bats +++ b/test/secrets.bats @@ -73,7 +73,7 @@ load test_helper run "$SECRETS_BIN" push testproj [ "$status" -eq 1 ] - [[ "$output" == *"No .env"* ]] + [[ "$output" == *"No secret files"* ]] } @test "push errors with missing key" { @@ -105,6 +105,44 @@ load test_helper [ "$status" -eq 0 ] } +@test "push encrypts .dev.vars files" { + init_with_remote + create_project_dir testproj + echo "CF_SECRET=wrangler123" > "$WORK_DIR/testproj/.dev.vars" + + run "$SECRETS_BIN" push testproj + [ "$status" -eq 0 ] + [ -f "$SECRETS_DIR/testproj/.env.age" ] + [ -f "$SECRETS_DIR/testproj/.dev.vars.age" ] +} + +@test "pull decrypts .dev.vars files" { + init_with_remote + create_project_dir testproj + echo "CF_SECRET=wrangler123" > "$WORK_DIR/testproj/.dev.vars" + "$SECRETS_BIN" push testproj >/dev/null 2>&1 + + local pull_dir="$WORK_DIR/pull-devvars" + mkdir -p "$pull_dir" + cd "$pull_dir" + + run "$SECRETS_BIN" pull testproj + [ "$status" -eq 0 ] + [ "$(cat "$pull_dir/.dev.vars")" = "CF_SECRET=wrangler123" ] +} + +@test "pre-commit blocks plaintext .dev.vars files" { + init_with_remote + cd "$SECRETS_DIR" + + echo "LEAKED=true" > .dev.vars + git add -f .dev.vars + + run git commit -m "should fail" + [ "$status" -eq 1 ] + [[ "$output" == *"Plaintext"* ]] +} + # ─── pull ────────────────────────────────────────────────────────────── @test "pull decrypts files correctly" { @@ -252,6 +290,137 @@ load test_helper [ "$status" -eq 0 ] } +# ─── clear ───────────────────────────────────────────────────────────── + +@test "clear removes plaintext secret files" { + init_with_remote + create_project_dir testproj + echo "CF_SECRET=wrangler123" > "$WORK_DIR/testproj/.dev.vars" + + # Verify files exist + [ -f "$WORK_DIR/testproj/.env" ] + [ -f "$WORK_DIR/testproj/.env.staging" ] + [ -f "$WORK_DIR/testproj/.dev.vars" ] + + run "$SECRETS_BIN" clear + [ "$status" -eq 0 ] + [[ "$output" == *"Cleared 3"* ]] + + # Files should be gone + [ ! -f "$WORK_DIR/testproj/.env" ] + [ ! -f "$WORK_DIR/testproj/.env.staging" ] + [ ! -f "$WORK_DIR/testproj/.dev.vars" ] +} + +@test "clear does nothing when no secret files exist" { + mkdir -p "$WORK_DIR/empty" + cd "$WORK_DIR/empty" + + run "$SECRETS_BIN" clear + [ "$status" -eq 0 ] + [[ "$output" == *"No secret files"* ]] +} + +@test "clear does not remove non-secret files" { + mkdir -p "$WORK_DIR/mixed" + cd "$WORK_DIR/mixed" + echo "SECRET=yes" > .env + echo "config" > .envrc + echo "other" > app.js + + run "$SECRETS_BIN" clear + [ "$status" -eq 0 ] + [ ! -f "$WORK_DIR/mixed/.env" ] + [ -f "$WORK_DIR/mixed/.envrc" ] + [ -f "$WORK_DIR/mixed/app.js" ] +} + +@test "clear --workspaces removes secrets from all workspaces" { + local mono + mono=$(create_monorepo) + cd "$mono" + + # Verify files exist + [ -f "$mono/.env" ] + [ -f "$mono/apps/web/.env.staging" ] + [ -f "$mono/apps/api/.env" ] + + run "$SECRETS_BIN" clear --workspaces + [ "$status" -eq 0 ] + [[ "$output" == *"Cleared"* ]] + + # All should be gone + [ ! -f "$mono/.env" ] + [ ! -f "$mono/apps/web/.env.staging" ] + [ ! -f "$mono/apps/api/.env" ] +} + +# ─── run ─────────────────────────────────────────────────────────────── + +@test "run pulls secrets, runs command, then clears" { + init_with_remote + create_project_dir testproj + "$SECRETS_BIN" push testproj >/dev/null 2>&1 + + # Remove plaintext files + rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging" + + # Run a command that reads the secret + run "$SECRETS_BIN" run cat .env + [ "$status" -eq 0 ] + [[ "$output" == *"SECRET_KEY=abc123"* ]] + + # After run completes, plaintext files should be cleared + [ ! -f "$WORK_DIR/testproj/.env" ] + [ ! -f "$WORK_DIR/testproj/.env.staging" ] +} + +@test "run clears secrets even if command fails" { + init_with_remote + create_project_dir testproj + "$SECRETS_BIN" push testproj >/dev/null 2>&1 + + # Remove plaintext files + rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging" + + # Run a command that will fail (set +e so bats captures it) + run "$SECRETS_BIN" run false + [ "$status" -ne 0 ] + + # Secrets should still be cleared + [ ! -f "$WORK_DIR/testproj/.env" ] + [ ! -f "$WORK_DIR/testproj/.env.staging" ] +} + +@test "run errors with no command" { + run "$SECRETS_BIN" run + [ "$status" -eq 1 ] + [[ "$output" == *"Usage"* ]] +} + +@test "run passes arguments through to command" { + init_with_remote + create_project_dir testproj + "$SECRETS_BIN" push testproj >/dev/null 2>&1 + rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging" + + # Run with multiple args + run "$SECRETS_BIN" run ls -la .env + [ "$status" -eq 0 ] + [[ "$output" == *".env"* ]] +} + +@test "run supports -- separator" { + init_with_remote + create_project_dir testproj + "$SECRETS_BIN" push testproj >/dev/null 2>&1 + rm "$WORK_DIR/testproj/.env" "$WORK_DIR/testproj/.env.staging" + + run "$SECRETS_BIN" run -- cat .env + [ "$status" -eq 0 ] + [[ "$output" == *"SECRET_KEY=abc123"* ]] +} + # ─── workspaces ──────────────────────────────────────────────────────── # Helper: create a monorepo with package.json workspaces @@ -348,5 +517,5 @@ EOF run "$SECRETS_BIN" push --workspaces [ "$status" -eq 1 ] - [[ "$output" == *"No .env files"* ]] + [[ "$output" == *"No secret files"* ]] }