---
description: "Teaches agents to run CLI commands, scripts, and tool invocations without leaking secrets through stdout, process arguments, shell history, or execution traces. Covers env-var hygiene, stdin-based secret passing, history suppression, log discipline, and tool-specific patterns for curl, git, Docker, and CI/CD pipelines."
alwaysApply: true
---


# Secure CLI Execution

Teaches agents to run CLI commands, scripts, and tool invocations without exposing
secrets, API keys, or credentials through stdout, process argument lists, shell
history, execution traces, or any other observable channel.

## Why This Matters

Secrets can leak through more channels than most agents account for:

| Channel | Example leak | Risk |
|---------|-------------|------|
| stdout/stderr | `echo $API_KEY` | Captured in traces, logs, terminal history |
| Process args | `curl --token abc123` | Visible in `ps aux`, `/proc/<pid>/cmdline` |
| Shell history | any command with secret in arg | Persisted to `~/.bash_history` |
| Temp files | `echo $SECRET > /tmp/key` | World-readable by default, persists on disk |
| Env var dumps | `env` or `printenv` | Prints every variable including secrets |
| Error messages | stack traces that include config | Framework errors may echo config values |

## Core Rules

### Rule 1 — Never pass secrets as command arguments

Process argument lists are public on POSIX systems.

```bash
# BAD — visible in ps aux and shell history
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data
npx some-cli --api-key "$STRIPE_KEY" deploy
docker build --build-arg SECRET_KEY="$SECRET_KEY" .

# GOOD — reference the env var; the shell expands it but it never touches args
# curl reads Authorization from env via a config file
curl -K <(echo "header = \"Authorization: Bearer $TOKEN\"") https://api.example.com/data

# Or use a tool that reads directly from env
STRIPE_KEY="$STRIPE_KEY" npx some-cli deploy
```

### Rule 2 — Reference env vars; never echo or print their values

Checking that a variable is set does not require printing it.

```bash
# BAD — emits the secret value
echo "Using key: $ANTHROPIC_API_KEY"
echo $STRIPE_SECRET_KEY
printenv AWS_SECRET_ACCESS_KEY

# GOOD — check presence without revealing value
if [ -z "$ANTHROPIC_API_KEY" ]; then
  echo "ANTHROPIC_API_KEY is not set" >&2
  exit 1
fi
echo "ANTHROPIC_API_KEY is set (${#ANTHROPIC_API_KEY} chars)"

# Check length only — confirms the var exists and is non-trivial
echo "Key length: $(printenv ANTHROPIC_API_KEY | wc -c) chars"
```

### Rule 3 — Use stdin patterns for password-style inputs

Many CLIs accept secrets via stdin to avoid argument exposure.

```bash
# BAD — password in args
docker login --username user --password "$DOCKER_PASSWORD" registry.example.com
gh auth login --with-token "$GH_TOKEN"

# GOOD — pipe through stdin
echo "$DOCKER_PASSWORD" | docker login --username user --password-stdin registry.example.com
echo "$GH_TOKEN" | gh auth login --with-token

# GOOD — process substitution (bash/zsh only)
git clone https://user:$(cat <(printenv GIT_PASSWORD))@github.com/org/repo.git
```

### Rule 4 — Suppress shell history for sensitive sessions

```bash
# Prevent the current session's commands from being saved
unset HISTFILE

# Or prefix commands with a space (requires HISTCONTROL=ignorespace in bashrc)
 export SECRET_KEY="actual-value-here"   # leading space — not saved to history

# For a block of sensitive operations
(
  unset HISTFILE
  # ... sensitive commands here ...
)
```

### Rule 5 — Secure temporary credential files

When a tool absolutely requires a file on disk:

```bash
# BAD — world-readable temp file, never cleaned up
echo "$PRIVATE_KEY" > /tmp/key.pem
some-tool --key /tmp/key.pem

# GOOD — restricted permissions, guaranteed cleanup
KEY_FILE=$(mktemp)
chmod 600 "$KEY_FILE"
trap "rm -f '$KEY_FILE'" EXIT
echo "$PRIVATE_KEY" > "$KEY_FILE"
some-tool --key "$KEY_FILE"
# trap fires on exit — file always removed
```

### Rule 6 — Log outcomes, never secret values

Execution traces and logs should record *what happened*, not *which credential was used*.

```bash
# BAD — logs the token
echo "Calling API with token: $API_TOKEN"
curl -v -H "Authorization: Bearer $API_TOKEN" https://api.example.com

# GOOD — log the outcome
echo "Calling API..." >&2
RESPONSE=$(curl -s -o /dev/stdout -w "%{http_code}" \
  -H "Authorization: Bearer $API_TOKEN" https://api.example.com)
STATUS="${RESPONSE: -3}"
BODY="${RESPONSE:0:${#RESPONSE}-3}"
echo "API response: HTTP $STATUS"
```

### Rule 7 — Never hardcode secrets in scripts or skill content

Scripts that are committed to version control, shared, or published as skills must
contain zero hardcoded credentials. Use env var references everywhere.

```bash
# BAD — hardcoded in script (never do this)
STRIPE_KEY="sk_live_<your-key-here>"
curl -u "$STRIPE_KEY:" https://api.stripe.com/v1/charges

# GOOD — always from environment
: "${STRIPE_SECRET_KEY:?STRIPE_SECRET_KEY must be set}"
curl -u "$STRIPE_SECRET_KEY:" https://api.stripe.com/v1/charges
```

## Tool-Specific Patterns

### curl

```bash
# Use a netrc file (chmod 600) instead of --user flag
echo "machine api.example.com login token password $API_KEY" > ~/.netrc
chmod 600 ~/.netrc
curl --netrc https://api.example.com/endpoint

# Use --config with process substitution to avoid the header in args
curl -K <(echo "header = \"Authorization: Bearer $TOKEN\"") https://api.example.com
```

### git

```bash
# Use a credential helper — never embed tokens in remote URLs
git config --global credential.helper store   # or 'osxkeychain' / 'manager'
echo "https://token:$GH_TOKEN@github.com" | git credential approve

# For CI: use GIT_ASKPASS
export GIT_ASKPASS=echo
export GIT_PASSWORD="$GH_TOKEN"
git clone https://user@github.com/org/repo.git
```

### Docker

```bash
# Pass secrets at runtime, not build time
# BAD — baked into the image layer
docker build --build-arg API_KEY="$API_KEY" .

# GOOD — mount a secret at build time (BuildKit, never in image layers)
DOCKER_BUILDKIT=1 docker build --secret id=api_key,env=API_KEY .
# In Dockerfile: RUN --mount=type=secret,id=api_key ...

# GOOD — inject at runtime only
docker run -e API_KEY="$API_KEY" myimage
```

### GitHub Actions / CI

```yaml
# Secrets are masked in logs automatically when referenced via secrets context
- name: Call API
  env:
    API_KEY: ${{ secrets.API_KEY }}
  run: |
    # $API_KEY is masked in output — GitHub replaces it with ***
    curl -H "Authorization: Bearer $API_KEY" https://api.example.com

# Add extra masking for derived values
- run: |
    DERIVED="${API_KEY:0:8}-suffix"
    echo "::add-mask::$DERIVED"
    echo "Derived prefix set"
```

### Node.js / Python

```javascript
// Node — read from process.env, never log the value
const apiKey = process.env.ANTHROPIC_API_KEY
if (!apiKey) throw new Error('ANTHROPIC_API_KEY is required')
console.log(`API key configured (${apiKey.length} chars)`)  // length only
```

```python
# Python — use os.environ, validate early, never print
import os
api_key = os.environ["ANTHROPIC_API_KEY"]  # raises KeyError if missing
print(f"API key configured ({len(api_key)} chars)")  # length only
```

## When Operating as an Agent in a CLI Environment

If you are an AI agent executing shell commands, follow these additional rules:

1. **Assume traces are visible.** Verification traces, stdout captures, and debug logs
   may be read by other users. Treat every line of output as potentially public.

2. **Prefer `-z` checks over echo.** When verifying env vars exist, use
   `[ -n "$VAR" ]` or `[ -z "$VAR" ]` — never `echo $VAR`.

3. **Report key length, not key value.** In trace output, confirm a credential is
   present by its length: `"API key: ${#ANTHROPIC_API_KEY} chars set"`.

4. **Never confirm the value matches.** Don't log "key starts with sk-ant-" —
   that's half the secret. Log "key format validated" or "key accepted by API".

5. **Treat AbortError / timeout as secret-safe.** If a network call fails, log the
   HTTP status and error message, not the headers that were sent.

6. **Scope secrets to the narrowest environment.** Set env vars inside subshells
   `(SECRET=x command)` rather than exporting to the whole session.

## Checklist Before Running Sensitive Commands

- [ ] Is the secret referenced as `$VAR_NAME` rather than its literal value?
- [ ] Will the command appear in `ps aux` with the secret visible?
- [ ] Does any `echo` or `print` statement emit the actual secret value?
- [ ] Are temp files created with `chmod 600` and a `trap ... EXIT` cleanup?
- [ ] Does the log output contain only outcomes (HTTP status, boolean pass/fail)?
- [ ] Will the shell history record the secret? (use `unset HISTFILE` if needed)

## Attribution

Part of the SkillSlap security toolkit. Maintained by the community.
Published at skillslap.com — install via MCP or `skillslap install secure-cli-execution`.
