nopeek

August 1, 2026 ยท View on GitHub

built with vite+ tested with vitest

CLI for reducing accidental secret disclosure in LLM coding sessions. nopeek gives agents a normal workflow for secret-dependent commands without requiring secret values to be pasted into prompts or printed by the loading step.

Quick Start

For Pi and other harnesses where each tool call starts a fresh shell, use run so loading and execution happen in one child process:

"use npx nopeek run .env --only DATABASE_URL -- sh -c 'psql \"$DATABASE_URL\" -c \"SELECT count(*) FROM users\"'"

"use npx nopeek run .env --only STRIPE_KEY -- sh -c 'curl -H \"Authorization: Bearer $STRIPE_KEY\" https://api.example.com'"

run reports no secret values itself. The child command can still print a secret, so choose commands and flags that do not dump their environment, credentials, or verbose authentication data.

If your harness supports persistent env-file injection, load can make selected variables available to later commands:

"use npx nopeek load .env --only DATABASE_URL,API_KEY, then use those variables by name"

How It Works

Shell output visible to an LLM coding agent may be sent to a model provider and retained there. Agents also have a habit of inspecting .env files or echoing variables when credentials are needed. nopeek provides a lower-risk path for routine work:

  1. It parses the requested secret file locally.
  2. run gives selected values only to one child process, or load uses the best environment-loading method available.
  3. nopeek's normal status output reports key names and loading state, not values.

This reduces accidental disclosure. It does not make secrets inaccessible to the agent or to the child command that receives them.

Important: Your agent does not know about nopeek unless you mention it. You do not need to spell out the full command, just mention {npx,pnpx,bunx} nopeek and the agent can discover the rest.

Optional agent reminders

If you use a harness with persistent system-prompt extensions, add a small reminder so the model reaches for nopeek automatically instead of needing the Quick Start text in every prompt. For Pi users, the @spences10/pi-nopeek extension does this:

pi install npm:@spences10/pi-nopeek

my-pi already includes this reminder by default. Its separate pi-redact extension also applies best-effort, last-mile redaction to tool results before model context. Those are optional defense-in-depth features, not capabilities of the standalone nopeek CLI. nopeek remains harness-agnostic and works anywhere when you mention it in the session.

load reports one of four methods:

MethodContextAvailability
env_fileHarness exposes an env-file injection targetFuture commands in that session can use the variables
source_fileAgent session without env-file injectionA self-removing 0600 file in a verified per-user 0700 root is created
name_onlyRegular defaultOnly key names are reported; no assignments or values are emitted
exportNon-JSON output with --allow-valuesAssignments are printed; variables exist only if the parent shell evaluates them

load reports its method and whether future commands can see the variables. It cannot mutate a parent shell. Prefer nopeek run or use the reported safe next step.

Usage

No install needed. Your agent runs it directly via npx:

npx nopeek load .env
npx nopeek load .env --only DATABASE_URL
npx nopeek run .env --only API_KEY -- sh -c 'curl -H "Authorization: Bearer $API_KEY" https://api.example.com'
npx nopeek set MY_API_KEY --from-env
npx nopeek status

The safe default for an LLM coding session is run, or load when status confirms env-file injection is available. Commands that emit shell assignments require extra care, as described below.

Installation trust

npx nopeek is convenient for evaluation, but npm may download code when the requested package is not already available locally. For production-secret work, prefer a reviewed exact version in a trusted project lockfile:

pnpm add --save-exact nopeek@0.0.15
pnpm install --frozen-lockfile
pnpm exec nopeek run .env --only API_KEY -- your-command

After the package is present in pnpm's local store, a locked checkout can use pnpm install --offline --frozen-lockfile followed by pnpm exec nopeek .... A compromised package, dependency, shell, child process, or user account is outside nopeek's protection.

npm trusted-publishing provenance is not currently enabled because this repository uses a maintainer-run Changesets publish command rather than an OIDC release workflow. CI instead builds, installs, and executes the packed artifact to catch packaging regressions; provenance should only be enabled alongside a reviewed trusted-publishing workflow.

Commands

load - Load secrets from .env or .tfvars files

npx nopeek load .env
npx nopeek load .env --only DATABASE_URL,API_KEY
npx nopeek load .env --shell bash --allow-values  # trusted shells only
npx nopeek load .env --persist  # also save to config for future sessions
npx nopeek load terraform.tfvars --only prod_password
npx nopeek load production.tfvars.json --only db_password

Supported files use deliberately strict, fail-closed grammars. A malformed or unsupported value rejects the whole file before --only filtering or any secret-loading side effect.

  • .env and .env.*: blank lines, # comment lines, and [export ]NAME=VALUE assignments are supported. Names match [A-Za-z_][A-Za-z0-9_]*; export is recognized only as a separate prefix, so export, exported, and export_token remain valid names. Whitespace around = and CRLF are accepted. Values may be unquoted, single quoted (literal), or double quoted with \\, \", \n, \r, and \t escapes. In unquoted values, # starts a comment only after whitespace; a value-leading # and # inside either quote style are data. A closed quoted value may only be followed by whitespace or a whitespace-prefixed # comment. Duplicate names, prototype-sensitive output names (__proto__, prototype, constructor), a UTF-8 BOM, expansion, multiline values, unknown escapes, malformed quotes, and other shell syntax are rejected.
  • .tfvars: top-level assignments and nested object/map assignments are supported when every leaf is a double-quoted string. Identifier or quoted keys, #/// line comments, whitespace-separated multiline or same-line maps, and the same basic escapes as above are accepted. Nested leaf names are flattened to their leaf key for environment loading; duplicate assignments, prototype-sensitive leaf names, or any resulting leaf-key collision reject the file. Commas, heredocs, interpolation, expressions, references, functions, dynamic keys, lists, numbers, booleans, null, multiline strings, and a BOM are rejected.
  • .tfvars.json: the root must be a JSON object whose leaves are strings; nested objects use the same collision-checked leaf-key flattening. Only JSON whitespace (space, tab, CR, LF) is accepted. Duplicate members at any depth, prototype-sensitive leaf names, flattened collisions, arrays, numbers, booleans, null, trailing content, and a BOM are rejected.

The --persist flag saves keys to ~/.config/nopeek/config.json so a SessionStart hook can auto-inject them on future sessions.

load returns method and contains_values fields in JSON output. Structured JSON never contains values. If method is env_file, keys are available to future session commands. source_file returns a safe path to source; name_only may return an explicitly gated next_command, for example:

eval "$(npx nopeek load .env --shell bash --allow-values)"
source <(npx nopeek load .env --shell bash --allow-values)
npx nopeek load .env --shell fish --allow-values | source

--shell supports bash, zsh, and fish, but assignment output requires the explicit --allow-values opt-in. --shell takes precedence over --json: that combination emits raw shell assignments, not JSON. This mode warns on stderr and writes secret values to stdout for a shell to consume. Detected LLM agent sessions reject --allow-values; use run, env-file injection, or the generated source file instead. Without opt-in, non-JSON output is name-only even in unknown non-TTY harnesses.

run - Run one command with loaded secrets

npx nopeek run .env --only API_KEY -- node ./script.js
npx nopeek run .env --only INGEST_TOKEN,FEEDGEN_SERVICE_URL -- sh -c 'curl -H "Authorization: Bearer $INGEST_TOKEN" "$FEEDGEN_SERVICE_URL"'
npx nopeek run terraform.tfvars --only prod_password -- ./deploy.sh
npx nopeek run production.tfvars.json --only DATABASE_URL -- sh -c 'psql "$DATABASE_URL" -c "select 1"'

run injects selected keys into only the child process environment and preserves the child command exit code. Use sh -c when you need shell expansion, pipes, redirects, or inline $VARIABLE expansion inside the child process. The child retains normal access to its environment and stdout: avoid env, printenv, shell tracing, verbose HTTP authentication, and other behavior that can print credentials.

set - Store a secret key

npx nopeek set MY_API_KEY --from-env  # read from current shell env
npx nopeek set MY_API_KEY             # interactive prompt (TTY only)

Stores to ~/.config/nopeek/config.json with 0600 permissions inside an owned 0700 directory. nopeek rejects symlinks, wrong ownership, non-regular files, corrupt JSON, and invalid config structure without overwriting the original. Missing config reads do not create directories. Safe owned permission drift is repaired through verified file descriptors, and destinations are revalidated immediately before atomic replacement. These checks do not protect against a process running as the same user racing the final filesystem operation. This is plaintext at rest; use set/--persist only for secrets you are comfortable storing in your user config.

Note: --value is rejected inside detected LLM agent sessions. The value would appear in the conversation. Use --from-env instead.

list - Show available keys

npx nopeek list

Shows key names and sources without values.

remove - Remove a stored key

npx nopeek remove MY_API_KEY

init - Audit cloud CLI authentication

npx nopeek init

Detects installed cloud CLIs and reports whether their current authentication needs migration. This command is advisory: it does not change cloud CLI configuration, and run/load do not apply cloud profiles. Older nopeek versions stored unused profile mappings; running init removes those obsolete entries without affecting credentials or the cloud CLIs themselves.

For a reported migration, configure the relevant CLI directly, remove the inline credential from your shell startup files, open a fresh shell, and rerun nopeek init:

  • AWS: aws configure --profile <name>, then set AWS_PROFILE=<name>.
  • hcloud: hcloud context create <name>, then use hcloud context use <name>.
  • gcloud: gcloud config configurations create <name>, then use gcloud config configurations activate <name>.
  • Azure: sign in with az login; select the account with az account set --subscription <name-or-id>.
CLISafer patternDetection
awsNamed profiles (AWS_PROFILE)~/.aws/credentials + env vars
hcloudNamed contexts (HCLOUD_CONTEXT)~/.config/hcloud/cli.toml
gcloudNamed configurations (CLOUDSDK_ACTIVE_CONFIG_NAME)active gcloud account + inline credential env vars
azAzure CLI cached login + active subscriptionaz account show + inline credential env vars

status - Show current state

npx nopeek status

Shows session type, stored keys, and detected CLIs. A legacy CLI profiles section may appear until nopeek init removes mappings written by older versions; those mappings are never applied to commands.

audit - Scan for exposed secrets

npx nopeek audit
npx nopeek audit ./path/to/dir

Recursively scans .env*, .tfvars, and .tfvars.json files and reports pattern categories (AWS keys, bearer tokens, API keys, private keys, connection strings, etc.) without printing matched values. It uses Git itself for nested .gitignore rules and negation, and reports each source as tracked, untracked, ignored, or unknown. unknown is used outside a repository, when Git is unavailable, or when classification fails. Example, sample, and template files are not assumed safe: files whose parsed values look live or whose contents match known secret patterns are flagged, while a narrow set of obvious whole-value placeholders is reported as a heuristic placeholder assessment.

The scan is a bounded heuristic, not proof that a repository is safe. It does not follow symlinks; skips dependency, build, VCS metadata, and framework output directories; visits at most 10,000 directories to a depth of 32; examines at most 1,000 candidates; and reads at most 1 MiB per file. Unreadable, binary, oversized, malformed, symlinked, or limit-exceeding candidates are reported without source contents and make the scan fail closed. Only supported secret-source filenames and known secret patterns are checked.

Security

  • Key name validation - env key names are validated against ^[a-zA-Z_][a-zA-Z0-9_]*$ to prevent shell injection
  • Secure file permissions - config dir is 0700, config file is 0600, temp env files are 0600
  • Atomic writes - config is written via temp file + rename to prevent corruption
  • Key validation before output - invalid env names are rejected before shell exports are printed
  • Name-only normal status output - run and default detected-agent loading report names/state rather than values; explicit shell-output modes and child commands can still print values
  • Plaintext config warning - persisted keys are local plaintext secrets protected by file permissions, not encryption

nopeek is a workflow control, not an access-control boundary. Deny rules are the enforcement layer in runtimes that support them. Use them to block agents from reading secret files or embedding credentials inline in commands:

{
	"permissions": {
		"deny": [
			// Block reading secret files
			"Read(.env)",
			"Read(*.env)",
			"Read(*.tfvars)",
			"Read(*credentials*)",
			"Read(*secret*)",
			"Read(**/.config/gcloud/**)",
			"Read(**/.azure/**)",
			"Read(*service-account*.json)",

			// Block cat/head on secret files
			"Bash(cat .env)",
			"Bash(cat *.env*)",
			"Bash(cat *tfvars*)",
			"Bash(cat *credentials*)",
			"Bash(cat *secret*)",

			// Block inline credentials in commands
			"Bash(PGPASSWORD*)",
			"Bash(*HCLOUD_TOKEN*)",
			"Bash(hcloud context create*)",
			"Bash(*GOOGLE_APPLICATION_CREDENTIALS*)",
			"Bash(*CLOUDSDK_AUTH_ACCESS_TOKEN*)",
			"Bash(*GOOGLE_OAUTH_ACCESS_TOKEN*)",
			"Bash(*AZURE_CLIENT_SECRET*)",
			"Bash(*ARM_CLIENT_SECRET*)",
			"Bash(*AZURE_PASSWORD*)",
			"Bash(*AZURE_ACCESS_TOKEN*)",
			"Bash(*ARM_ACCESS_KEY*)",

			// Block fetching secret values from cloud providers
			"Bash(*secretsmanager get-secret-value*)",
			"Bash(*hetzner*secret*)",
			"Bash(*hetzner*access_key*)",
			"Bash(gcloud auth print-access-token*)",
			"Bash(gcloud auth application-default print-access-token*)",
			"Bash(az account get-access-token*)",
		],
	},
}

Without these rules, an agent can still read a .tfvars or .env file directly and hardcode or print the values. Deny rules reduce that access; nopeek gives cooperative agents a practical alternative.

Threat Model and Non-Goals

nopeek is designed for a cooperative coding agent that needs to run a command with credentials while avoiding accidental disclosure during loading. It protects against common mistakes such as pasting a value into chat, reading an entire .env file for one key, or printing exports as an intermediate step when run/env-file injection is used.

nopeek does not protect secrets from:

  • an agent that deliberately reads the source file or prints variables
  • a child command that dumps its environment, traces commands, or logs credentials
  • a compromised nopeek package, dependency, shell, child process, or user account
  • another process with sufficient access to the same user-owned files

Additional limitations:

  • Pattern-based secret detection is best-effort. audit performs a bounded scan of .env*, .tfvars, and .tfvars.json sources for known patterns and cannot identify every secret or prove a file safe.
  • Agent detection is marker-based. An unknown harness may be treated like a regular shell; check nopeek status and avoid assignment-emitting modes in agent-visible output.
  • Temp files are briefly plaintext. source_file writes values to a unique 0600 file under the verified per-user 0700 directory <os.tmpdir()>/nopeek-<uid>/. The file removes itself after it is sourced; interrupted-session files older than 24 hours are removed safely on a later write, with only the removal count reported. nopeek refuses unsafe roots and never follows symlinks during cleanup. Files created by older versions under /tmp/nopeek/ are intentionally not touched; inspect and remove that legacy directory manually after upgrading.
  • Persisted keys are plaintext. set and load --persist store values in ~/.config/nopeek/config.json with 0600 permissions.
  • Redaction is separate and best-effort. Standalone nopeek does not redact arbitrary child output. Harness integrations such as my-pi may add a separate safety net, but cannot guarantee complete redaction.

Security integration tests

pnpm run build
pnpm run test:integration

The integration suite executes the built CLI in isolated home, config, temporary, env-file, and working directories. Bash is required for the portable CI subset. Zsh and fish assignment tests run when those shells are installed and otherwise appear as explicit skips. A separate Ubuntu release-gating job installs and requires bash, zsh, and fish, then runs the built-CLI assignment round-trip and canary assertions for all three; a missing supported shell fails that job. POSIX permission checks remain platform-dependent. Tests use synthetic canaries, timeouts, and cleanup rather than real credentials.

The run tests distinguish nopeek output from deliberate child output. A child explicitly instructed to print its injected environment can print a canary; that controlled child channel is outside nopeek's normal-output secrecy guarantee.

License

MIT