safety-op-creds
Skill7 starsChanged 3 months agoMITView on GitHub
Fetch credentials from 1Password via the `op` CLI and feed them to programs through bash process substitution (/dev/fd/N file descriptors) or `op run` env vars, so secrets never touch disk or the agent's tool output. Ships a `with-creds` wrapper plus a PreToolUse hook for Claude Code and Codex that blocks bare `op read` and other secret-printing op subcommands. Run `scripts/install.sh` after `npx skills add` for always-on protection; pass `--codex` for Codex.
- Reads credentials
In this repository51 other files it gives its agents.
- AGENTS.mdAGENTS.md
- skills/optimize-permissions/references/agents.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- acid-tripSkill
- audit-aws-costsSkill
- audit-memoriesSkill
- broSkill
- clean-ai-slopSkill
- cleanup-computerSkill
- code-tourSkill
- copywritingSkill
- decomposeSkill
- eli5Skill
- find-docsSkill
- gemini-deep-researchSkill
- investigate-repoSkill
- laconicSkill
- loose-endsSkill
- memory-insightsSkill
- nano-banana-generatorSkill
- optimize-permissionsSkill
- optimize-skill-activationSkill
- projectSkill
- prSkill
- quality-chaos-monkeySkill
- quality-cli-agent-friendly-auditSkill
- quality-dead-code-analyzerSkill
- quality-docs-updateSkill
- quality-perf-reviewSkill
- quality-project-healthSkill
- quality-worktree-setupSkill
- record-gifSkill
- reflect-on-conversationSkill
- review-codeSkill
- safety-dotenv-guardSkill
- safety-git-reset-guardSkill
- safety-rm-rf-guardSkill
- security-bumblebeeSkill
- security-gitleaksSkill
- security-openssfSkill
- security-scfwSkill
- security-snykSkill
- security-socketSkill
- ste100Skill
- storage-cleanupSkill
- testSkill
- transcribe-youtubeSkill
- travel-photo-stampSkill
- trip-plannerSkill
- zoomSkill
- __SKILL_NAME__Skill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 61
When bash sees <(command), it:
- Very specific: line 127
When the program reads its env vars natively (e.g.
- Very specific: line 241
Treat this as defense in depth, not an airtight guarantee.
- Very specific: line 274
Verify the hook is wired by asking the agent to run op read "op://Vault/Item/field".
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
safety-op-creds
Supports Claude Code and Codex. For an always-on Codex hook, run:
~/.codex/skills/safety-op-creds/scripts/install.sh --codexThen open /hooks in Codex and review and trust the hook.
The sanctioned alternative to .env files: store credentials in 1Password,
fetch them at runtime via the op CLI, and pass them to the consuming
program through a kernel pipe (/dev/fd/N via bash process substitution)
or as masked environment variables (via op run). Secrets never land on
disk and never appear in the agent's tool output.
Two pieces:
with-credswrapper — a small bash script inscripts/with-credsthat takes one or more--env NAME=op://...and/or--fd NAME=op://...flags plus a target command, then execs that command with the secrets wired in via process substitution orop run.- PreToolUse hook — blocks Bash tool calls that would leak secrets:
bare
op read,op item get --reveal,op item get --format=json,op injectto stdout/file, and subshell/eval bypasses of any of the above.
Pairs naturally with the safety-dotenv-guard skill: safety-dotenv-guard
prevents the wrong pattern (reading .env); safety-op-creds enables the right
one.
Why this exists
.env files on disk are the wrong default for AI-agent workflows:
- Once an agent reads
.env, the values live in its context and can leak into transcripts, summaries, and PR descriptions. - A
.envaccidentally committed to git is the canonical secret-leak incident; a.envin~/projects/is onegrep -r SECRET ~/away from a different incident.
The fix is to keep the secret out of the filesystem (and out of the
agent's stdout). 1Password stores the value; the op CLI fetches it on
demand; bash process substitution or op run delivers it to the consumer
through a kernel pipe or a child-process env var. The agent never sees
the literal bytes.
How the FD trick works
When bash sees <(command), it:
- Forks
commandas a subprocess and connects its stdout to a pipe. - Substitutes the
<(command)token in the surrounding command line with a path like/dev/fd/63. - The consumer opens that path and reads bytes that came from the subprocess's stdout.
The "file" at /dev/fd/63 is a handle on the running pipe — not an
inode in any filesystem. The secret lives in three places only: the
op subprocess's memory, the kernel pipe buffer, and the consumer
process's memory. It is never written to disk and never appears on the
agent's stdout, because the agent's tool output captures the outer
command's stdout, not what flows through the inner FD.
op run --env-file=... works similarly for env-var consumers: op
resolves op://... references inline, sets the resulting env vars in
the child process's memory, and execs the target. The masking in op
run additionally redacts secret values that the child happens to print
to stdout/stderr, replacing them with <concealed by 1Password>.
Usage
Env-var consumers (--env)
For programs that read credentials from environment variables:
with-creds --env AWS_ACCESS_KEY_ID=op://Vault/AWS/access_key_id \
--env AWS_SECRET_ACCESS_KEY=op://Vault/AWS/secret_access_key \
-- aws s3 ls
with-creds --env DATABASE_URL=op://Personal/Postgres/connection_string \
-- psql
with-creds --env GITHUB_TOKEN=op://Work/GitHub/token \
-- gh repo listUnder the hood: op run resolves the refs, sets the env vars in the
child process only, and masks any secret values that appear in the
child's stdout/stderr.
File-path consumers (--fd)
For programs that take a path to a key file, certificate, or token file:
with-creds --fd KEY=op://Personal/SSH/private_key \
-- ssh -i %KEY% user@host
with-creds --fd CERT=op://Work/Postgres/client_cert \
-- psql --set=sslcert=%CERT% "host=db.internal user=app"%NAME% must be the whole argument. The wrapper replaces each whole-
argument %NAME% with the corresponding <(op read "op://...") process
substitution and execs via bash -c. For embedded uses like
--cert=%CERT%, fall back to plain bash:
some-tool --cert=<(op read "op://Work/MyApp/cert")Mixing both modes
When the program reads its env vars natively (e.g. aws, gh, psql),
--env and --fd compose cleanly:
with-creds --env AWS_ACCESS_KEY_ID=op://Vault/AWS/access_key_id \
--env AWS_SECRET_ACCESS_KEY=op://Vault/AWS/secret_access_key \
--fd CA=op://Vault/AWS/ca_bundle \
-- aws --ca-bundle %CA% s3 lsGotcha: $VAR interpolation needs an inner shell
--env sets the variable in the child process. If the value has to
be spliced into an argv token (curl's Authorization header is the
canonical case), $VAR written directly in the command won't work:
the parent shell expands $VAR before with-creds runs (so the
secret is empty), and with-creds argv-quotes its arguments before
exec, so a literal $VAR can't be expanded by the child either.
Wrap the consumer in sh -c so the inner shell (which inherits the
env from op run) does the expansion:
with-creds --env API_KEY=op://Work/Service/api_key \
--fd CA=op://Work/Service/ca_bundle \
-- sh -c 'curl --cacert "$1" -H "Authorization: Bearer $API_KEY" https://api.example.com' _ %CA%%CA% is replaced by with-creds with the file-descriptor path and
arrives as $1 inside sh -c. $API_KEY is interpolated by the
inner shell from its own environment, where op run placed it.
Plain bash, no wrapper
The wrapper is convenience; the underlying patterns work directly:
# Process substitution — for file-path consumers
ssh -i <(op read "op://Personal/SSH/private_key") user@host
# `op run` — for env-var consumers
op run --env-file=secrets.template -- my-appBoth forms pass the hook unchanged.
What the hook blocks
The hook only matches Bash tool calls (other tools pass through).
Within Bash, it blocks:
- Bare
op read— anywhere except inside<( ... ).op read "op://Vault/X/y"— would print the secret to the agent.op read "op://..." > /tmp/x— would write the secret to disk.op read "op://..." | tee log— would duplicate the secret to stdout.VAR=$(op read "op://...")— captures the secret into shell memory, easy to accidentally echo. Usewith-credsor<( ... )instead.`op read "op://..."`— same as above (backtick form).
- Bare
op inject— anywhere except inside<( ... ). Resolvesop://...refs in a template and writes the result to stdout or to-o FILE, both of which expose secrets. op item get … --reveal— explicitly prints secret fields.op item get … --format json(or--format=json) — JSON output includes secret values inline.- Subshell / eval bypasses —
sh -c 'op read …',bash -c "op item get --reveal …",eval 'op inject …'. The hook scans the raw command before stripping quotes, so hiding the bad pattern inside a quoted-carg doesn't help.
What the hook allows
op readinside<( ... )— process substitution; the secret flows through/dev/fd/N, not stdout.op injectinside<( ... )— same reasoning; resolved template flows through the FD.op run— designed for safe secret consumption; masks values in the child's output by default.op signin,op whoami,op vault list,op item list,op account …— no secret values in their output.- Plain
op item get <item>— default output is metadata only; secret fields are concealed unless--revealor--format=jsonis passed. - Bundled
with-credsinvocations — the command text the agent produces contains no literalop read, so it passes the scan naturally; the wrapper handles secrets internally. - Legitimate quoted mentions —
git commit -m "doc op read usage",echo "op read example",grep -E "op read". The hook strips quoted substrings before scanning (the raw-bypass scan still catches the subshell variants of the same hiding trick). - Every non-Bash tool —
Read,Edit,Write,Grep,Glob, etc. pass through untouched.
Known limitations
Honest about what this skill cannot catch:
- Custom wrappers and aliases. A user-defined
1pass-fetchfunction that callsop readinternally bypasses the hook — only the outer command name is visible. The same applies to compiled binaries that invokeop. - Blind FDs. A program could open
/dev/stdinfrom inside a<(op read …)and write the bytes elsewhere. The hook can't observe child- process behavior. - Masking gaps in FD mode.
op runmasks secrets in the child's output. Process substitution does not — the secret bytes flow raw through the FD, and if the child decides to echo them, they appear in stdout. The hook does not (and cannot, generally) inspect what the child does with the FD's bytes. - Encoded references.
op read $(echo b3A6Ly8... | base64 -d)hides the reference behind a decode. The outer command is still blocked (it containsop read), but more exotic constructions might evade the regex. - Concurrent processes. The hook runs on the Claude Code tool boundary only; a separate shell session can run anything.
Treat this as defense in depth, not an airtight guarantee. Pair it
with .gitignore hygiene, OS keychain protection of the 1Password
session, the safety-dotenv-guard skill, and an honest threat model.
Install
npx skills add zcaceres/skills -s safety-op-creds
~/.claude/skills/safety-op-creds/scripts/install.sh
# Codex:
~/.codex/skills/safety-op-creds/scripts/install.sh --codexThe second step wires this skill's PreToolUse:Bash hook into
the host's JSON hook config so bare op read and other secret-printing
subcommands are blocked on every Bash call, not just when this skill is
active in context. The script is idempotent, backs up the target file
with a timestamp, and is a no-op if the hook is already wired. Flags:
--claude, --codex, --project, --target PATH. Codex users must open
/hooks and trust the hook. Requires jq.
Frontmatter hooks: blocks fire only while the skill is loaded into
context, so they're not real always-on protection — install.sh closes
that gap. See
safety-rm-rf-guard's Install section
for the full explanation.
For convenient use of the wrapper, symlink it onto your PATH:
ln -s ~/.claude/skills/safety-op-creds/scripts/with-creds ~/.local/bin/with-credsVerify the hook is wired by asking the agent to run
op read "op://Vault/Item/field"; the hook returns a PreToolUse deny
decision containing BLOCKED:.
Manual wiring (alternative)
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "<path>/safety-op-creds/scripts/run.sh" }
]
}
]
}
}On Windows, point at scripts\\run.cmd instead.
Prerequisites
opCLI version 2.x or later (install). The wrapper callsop whoamias a preflight; if it returns non-zero, the wrapper exits with a helpful error.- Signed in. On macOS, the recommended setup is Touch ID unlock via
the 1Password app — no master password lives on disk. Run
op signinonce per session if biometric unlock isn't enabled. - Bun runtime (only required to rebuild the hook binary from
source). Pre-built binaries for macOS arm64, Linux x64, and Windows
x64 ship in
scripts/bin/.
Out of scope for v1
- Service-account tokens (
OP_SERVICE_ACCOUNT_TOKEN). The token itself is a credential and should live in the macOS Keychain, fetched viasecurity find-generic-password. A future revision may add this. - 1Password Connect server. Useful for teams; ask if you need it.
opCLI plugin proxies. TheopCLI can shim other tools (e.g.op plugin run -- gh) so they pull secrets transparently. The hook only inspects the literal command; plugin behavior is opaque to it.
How it works
- Claude Code or Codex invokes
scripts/run.shbefore every matching shell tool call. run.shpicks the right bundled binary fromscripts/bin/for the host OS/arch (safety-op-creds-darwin-arm64,safety-op-creds-linux-x64, orsafety-op-creds-windows-x64.exe).- The binary reads the JSON tool payload from stdin and inspects
tool_input.command. - The command is first scanned raw for subshell/eval bypass patterns
(
bash -c '… op read …',eval '… op inject …', etc.). - Then quoted substrings are stripped so legitimate mentions in commit messages, echo strings, and grep patterns don't trip the scan.
<(op read …)and<(op inject …)substrings are then masked out — these are the sanctioned consumption patterns.- Anything left matching
op read,op inject,op item get … --reveal, orop item get … --format jsonreturns the sharedPreToolUseJSON deny contract on stdout and exits 0. The reason points the agent atwith-creds,<( ... ), orop run; both hosts reject the tool call without treating the hook itself as failed.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

