agents/skills/prompt-security-hardening/SKILL.md
Use when writing skills, CLAUDE.md files, agent prompts, or shell snippets that touch environment variables, API credentials, file creation, or git operations. Covers keeping secrets out of context, safe shell patterns, and credential exposure.
npx skillsauth add timofreiberg/dotfiles prompt-security-hardeningInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The agent's context window is sent to an API provider. Anything in context becomes data shared with that provider — and, often, with anything downstream that logs the conversation. The simplest model: any secret that enters the agent's context is a leaked secret.
This skill is the small set of patterns that keep credentials out of context, terminals, logs, and committed files. It applies whenever a directive involves shell commands, environment variables, secret-bearing files, or git operations.
When you need to know whether an env var is set, check existence — don't read the value.
# Safe: existence check, no value in output
[[ -v STRIPE_SECRET_KEY ]] && echo "set" || echo "not set"
# Also safe: works on older bash
[ -z "${STRIPE_SECRET_KEY+x}" ] && echo "not set" || echo "set"
# Unsafe — these put the value in your context
echo "$STRIPE_SECRET_KEY"
printenv STRIPE_SECRET_KEY
echo "Preview: ${STRIPE_SECRET_KEY:0:8}..." # partial values still leak entropy
echo "Length: ${#STRIPE_SECRET_KEY}" # length narrows the search space
env | grep STRIPE_SECRET_KEY # shows the value
A partial value or a length is still a leak. An 8-character prefix of an API key narrows the search space dramatically; a length confirms format.
When checking shell config files (~/.zshrc, ~/.bashrc, ~/.envrc),
match for the variable's presence without printing the matching line:
# Safe — tells you whether the export exists, nothing about the value
grep -q 'ANTHROPIC_API_KEY' ~/.zshrc && echo "found" || echo "not found"
# Unsafe — prints the full export line, value included
grep 'ANTHROPIC_API_KEY' ~/.zshrc
Code examples in directives shape the code agents produce next. Always reference env vars; never inline real or fake credentials.
# Safe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
# Unsafe — reproduces training-data patterns, even with "test" keys
stripe.api_key = "sk_live_..."
stripe.api_key = "sk_test_..."
# Safe
environment:
DATABASE_URL: ${DATABASE_URL}
# Unsafe
environment:
DATABASE_URL: postgresql://admin:password123@db:5432/myapp
Placeholder values like changeme, your-api-key-here, or
postgres://user:password@localhost/db look harmless but cost twice: they
train developers to put real values in the same spot, and they desensitize
secret-scanners (real alerts get lost in placeholder noise).
For committed .env.example files, prefer empty values:
# Safe — the shape is documented, no value is suggested
STRIPE_SECRET_KEY=
DATABASE_URL=
JWT_SECRET=
# Unsafe — fake credentials normalize the pattern
STRIPE_SECRET_KEY=sk_test_your_key_here
JWT_SECRET=change-this-to-something-secure
When creating any file that holds secrets — .env, .envrc, key files,
config with embedded tokens — set 0600 immediately, before populating it.
touch .env && chmod 600 .env
# now safe to write secrets to it
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
Default umask usually produces 0644 — world-readable. SSH refuses to use
keys with open perms; .env files have no such guardrail, so the
discipline has to come from the workflow.
Before creating a secret-bearing file, make sure git will ignore it.
git check-ignore -v .env || echo ".env" >> .gitignore
touch .env && chmod 600 .env
# Same pattern for direnv
git check-ignore -v .envrc || echo ".envrc" >> .gitignore
This applies to anything that will hold credentials: .env, .envrc,
secrets.conf, credentials.json, key files, MCP configs with embedded
tokens.
Tokens in URLs end up in server access logs, proxy logs, and browser
history. Tokens in command-line arguments are visible to other users via
ps aux.
# Safe — header via process substitution, invisible to ps aux and URL logs
curl -H @<(echo "Authorization: Bearer ${API_TOKEN}") https://api.example.com/data
# Acceptable — header keeps the token out of URL logs, but the expanded
# value is still visible in process lists (ps aux)
curl -H "Authorization: Bearer ${API_TOKEN}" https://api.example.com/data
# Unsafe — token in URL query string, logged everywhere
curl "https://api.example.com/data?api_key=${API_TOKEN}"
For git, avoid embedding tokens in clone URLs:
# Unsafe — token persists in .git/config and shell history
git clone "https://${GITHUB_TOKEN}@github.com/org/repo.git"
# Safer — credential helper (input must be `url=` attribute format; note
# this stores the token in plaintext in ~/.git-credentials and mutates
# global git config)
git config --global credential.helper store
echo "url=https://oauth2:${GITHUB_TOKEN}@github.com" | git credential-store store
git clone https://github.com/org/repo.git
For other CLIs that insist on a token argument with no header or stdin alternative, the same process-substitution trick shrinks the exposure window.
When constructing shell commands from file contents, tool output, or user input, quote everything and validate the input shape.
# Unsafe — unquoted, vulnerable to metacharacter injection
FILENAME=$(some_tool_output)
cat $FILENAME
# Safe
cat "$FILENAME"
# Unsafe — user input interpolated raw
USER_INPUT="$1"
find . -name $USER_INPUT
# Safe — validated and quoted
USER_INPUT="$1"
[[ "$USER_INPUT" =~ ^[a-zA-Z0-9._-]+$ ]] || { echo "invalid input" >&2; exit 1; }
find . -name "$USER_INPUT"
For SQL inside shell, use parameter binding rather than string interpolation:
# Unsafe
psql -c "SELECT * FROM users WHERE name = '$USERNAME'"
# Safe — psql variable binding (must go via stdin: -c strings don't
# expand psql variables)
echo "SELECT * FROM users WHERE name = :'username'" | psql --variable="username=$USERNAME"
Reading a file pulls its contents into the context window. Before reading, ask whether the file might contain secrets.
Files that commonly do:
.env, .envrc, *.env.*credentials.json, secrets.*, *-key.pemenv blocks.env files~/.aws/credentials, ~/.netrc, ~/.npmrc with auth tokensWhen debugging configuration, you usually need structure, not values:
wc -l .env # number of entries
grep -c '=' .env # number of key=value pairs
grep -E '^(export )?[A-Z_]+=' .env | sed 's/^export //' | cut -d= -f1 # list keys, not values
stat .env # metadata
When the value itself is in question (is this key valid? expired?), still don't print it — exercise it instead (make an authenticated call and check the status code) or ask the user to check it themselves.
When writing a skill, CLAUDE.md, or agent prompt:
chmod 600 and a gitignore check.| Need | Safe | Unsafe |
|----------------------------|-------------------------------------------------------|-----------------------------------------|
| Check env var exists | [[ -v VAR ]] or [ -z "${VAR+x}" ] | echo $VAR, printenv VAR |
| Use credential in code | os.environ["KEY"] | key = "sk_live_..." |
| Create secret file | touch f && chmod 600 f | echo "secret" > f (mode 644) |
| Pre-commit safety | git check-ignore -v .env first | Create .env and hope |
| API auth | -H @<(echo "Authorization: Bearer $TOKEN") | ?api_key=$TOKEN in URL |
| Git clone with token | Credential helper or GIT_ASKPASS | https://[email protected]/... |
| Inspect config | grep '^KEY=' f \| cut -d= -f1 | cat f, source f |
| Use a shell variable | "$VAR" (quoted) | $VAR (unquoted) |
databases
Use when a judgment forms during work that a future session would benefit from — a fork you resolved, a correction from the user, a wrong assumption about the environment, something you had to rediscover. Appends one timestamped entry to the journal staging dir.
development
Use when starting your work day: groom the todo list to a trusted state, archive finished work, surface today's candidates, and propose a concrete first move. Stab-then-confirm, ~5 min.
data-ai
Use when reviewing local changes — the working-copy diff, a branch, a commit, or a GitHub PR by number — with a fresh subagent that returns a structured findings report.
tools
Use when a question needs current internet information — docs, news, releases, prices. Prefer a built-in web search tool for quick lookups if the harness has one; this script returns a model-summarized answer with source URLs and works without one.