Secrets broker for coding agents

Seven verbs. Not one of them prints a secret.

keyless puts a secret into one child process's environment and nowhere else — not stdout, not scrollback, not the transcript. There is deliberately no verb that prints a value, and there never will be. That absence is the whole product.

01  you type keyless run -s STRIPE_KEY -- sh -c 'curl -H "$STRIPE_KEY" https://api.example.com' the name only
02  keyless resolves STRIPE_KEY from the store and spawns the child with it in the environment never printed
03  the child curl reads $STRIPE_KEY from its own environment and sends the header injected ✓
stdout
nothing
scrollback
nothing
shell history
nothing
the transcript
nothing
the child's environ
the value

Two states, no third — INJECTED or DEGRADED. A partial injection does not exist. keyless get is not a typo you can make; it does not exist. MIT licensed · Rust · five direct dependencies · 24 packages in the lock file.

01measure your own

Your number,
not ours.

A credential reaches a command in four shapes. This page will not tell you how often it happens on someone else's laptop — count it on yours.

The four shapes

Each one puts the value itself on the command line, where the shell, the history file and the transcript all record it.

  • url-embeddedhttps://user:VALUE@host
  • env assignmentexport TOKEN=VALUE
  • cli flag--token VALUE
  • http headerAuthorization: Bearer VALUE

all four are one primitive wide — spawn a child with the secret in its environment

Count them in your own history

Standard tools, nothing installed. Each line prints a count and never prints a matching line — the same rule the tool follows.

# swap in ~/.bash_history, or any transcript directory
H=~/.zsh_history

grep -acE 'https?://[^ /]*:[^ /@]*@' "$H"          # url
grep -acE '[A-Z_]{4,}=[^ ]{12,}' "$H"              # env
grep -acE '\-\-(token|key|secret|password)[= ][^ -][^ ]*' "$H"   # flag
grep -acE '[Aa]uthorization: *(Bearer|Basic) [^ "]+' "$H"       # header

four counts. that is your number, and it is the only one that should persuade you

The counts are lines, not secrets — a shape is checkable without anyone reading a value, which is why this page counts shapes and never values.  ·  A correctly wrapped keyless run matches none of the four, so the recipe does not count the fix as the problem.  ·  The CLI-flag shape is the documented reason keyless put has no --value flag.

02where a secret goes

Four spellings.
One works.

The instrument is driven by the command, not by a secret. Nothing here asks you to paste a credential — asking is the offence, whatever the client-side code does.

injected what the child actually receives
# single-quote the body, and keep it inside sh -c
keyless run -s TOKEN -- sh -c 'curl -H "$TOKEN" https://api.example.com'

Only the INNER shell expands, and it expands from the environment keyless just handed it. This is the one spelling where the value reaches the request.

the argument carriesthe value
in scrollbacknothing
in shell historynothing
in the transcriptnothing
in the child environthe value
exit code0
exposed in1 place
basisdocumented behaviour
verdictcorrect

The exit code is 0 in all four rows. keyless never refuses: an unresolvable name warns on stderr, runs the child with an untouched environment, and forwards its exit code. A missing credential is a 401 at exit 0 — read the DEGRADED banner, because the exit code cannot tell you.

Two of these four spellings inject correctly and still send nothing useful.

The calling shell expands "$TOKEN" before keyless is ever executed, and keyless does not expand its own arguments. Neither fault is visible to the broker — by the time it runs, the argument is already empty or already literal. Single-quote the body and keep it inside sh -c, and the rule holds for every command you will ever wrap.

03twenty encodings

Twenty shapes.
Ten needles.

The masker generates twenty encodings of a secret, in a fixed order, and deduplicates. For a plain alphanumeric value, half of them are the same string.

the value being masked
not-a-real-key-4f2a9c1d
distinct needles
10 / 20
#encoderreads asrenderedsame as
01rawas storednot-a-real-key-4f2a9c1d
02lowercasecase-foldednot-a-real-key-4f2a9c1d= raw
03uppercasecase-foldedNOT-A-REAL-KEY-4F2A9C1D
04base64-stdthe usual base64bm90LWEtcmVhbC1rZXktNGYyYTljMWQ=
05base64-std-nopadGo writes thisbm90LWEtcmVhbC1rZXktNGYyYTljMWQ
06base64-urlurl-safe alphabetbm90LWEtcmVhbC1rZXktNGYyYTljMWQ== base64-std
07base64-url-nopada JWT segmentbm90LWEtcmVhbC1rZXktNGYyYTljMWQ= base64-std-nopad
08base32a TOTP seedNZXXILLBFVZGKYLMFVVWK6JNGRTDEYJZMMYWI===
09base32-nopadunpadded base32NZXXILLBFVZGKYLMFVVWK6JNGRTDEYJZMMYWI
10hex-lowerhexdump6e6f742d612d7265616c2d6b65792d3466326139633164
11hex-upperhexdump, shouting6E6F742D612D7265616C2D6B65792D3466326139633164
12hex-0x-lowera literal in code0x6e6f742d612d7265616c2d6b65792d3466326139633164
13hex-0x-uppera literal in code0x6E6F742D612D7265616C2D6B65792D3466326139633164
14url-queryspace becomes +not-a-real-key-4f2a9c1d= raw
15url-pathspace becomes %20not-a-real-key-4f2a9c1d= raw
16url-strictRFC 3986not-a-real-key-4f2a9c1d= raw
17json-minimalJSON.stringifynot-a-real-key-4f2a9c1d= raw
18json-htmlGo escapes & < >not-a-real-key-4f2a9c1d= raw
19json-slashPHP escapes /not-a-real-key-4f2a9c1d= raw
20json-ascii-onlyPython escapes énot-a-real-key-4f2a9c1d= raw

Rendered in your browser, by the same twenty encoders in the same order. The repo's test table is an independent oracle — its literals are generated by Python's standard library, not by the code under test.  ·  Split at every byte is still caught: the masker holds back bytes that could begin a needle and releases them the instant they cannot. Observed live against fold -w1. It is a prefix withhold, content-aware — not a suffix carry.

04what it does not catch

A filter,
not a control.

Three tokens defeat it: sh -c 'echo $TOKEN > /tmp/x'. By design. The threat model is a capable agent taking a shortcut, not an adversary.

Six shapes, driven at the real binary

  • the raw value, echoedmasked
  • contiguous hexmasked
  • one byte per write — fold -w1masked
  • gzipnot caught
  • xxd -p wrapped at 60 columnsnot caught
  • HTTP Basic — user:pass base64'd togetherlength-dependent

The Basic-auth limit, measured

The username is folded into the same base64 block, so whether the secret survives as a recognisable needle depends on how the bytes align.

  • username ab: — 3 bytescaught
  • username abcde: — 6 bytescaught
  • username apiuser: — 8 bytesprints in the clear

one username length in three, proven — not estimated

the number that hurts

keyless run never refuses.

An unresolvable name warns on stderr, runs the child with an untouched environment, and forwards the child's exit code. So a missing credential is a 401 at exit 0. Nothing in the exit status distinguishes a working secret from a missing one; only the DEGRADED banner does.

  • The child gets the real value. It can write it to a file, POST it anywhere, or print it. Masking filters what comes back out through the pipe; it does not constrain the process.
  • Nothing here survives sudo. If you are an admin on your own machine, this is a boundary against your sessions — not against you.
  • One part is a control, not a habit. The daemon's store is owned by a uid you are not: secrets.json at mode 0600, user _keyless. The install asks for sudo exactly once, to create that user, and nothing afterwards does. The audit log is readable by you and not writable by you, and each row is hashed as sha256(previous || row).
  • 15 tests are #[ignore]d and never run in CI — the entire live Proton Pass suite, which needs a real account. CI asserts ignored == 15 precisely so that stays visible rather than quietly drifting.
  • macOS only for the daemon. CI runs the Linux link and requires it to fail, on exactly four named XNU symbols. Linux is unshipped because it is untested, and the build says so out loud.
  • There is no "leaks prevented" counter on this page, and there never will be. A security product's success is a non-event. keyless cannot show you the leak that did not happen, and a number nobody can audit is exactly the wrong thing to put on this page.

Every verdict above was driven at the real binary, not read from a document.  ·  Three of five masking limits carry a test. The other two do not, and this page will not claim otherwise.

05the hook pack

Four ways past.
All four named.

A Python hook pack rides alongside the binary and guards the paths an agent actually takes. An attack corpus is driven at it, and the suite fails in both directions — if a row that is not on the published survivor list gets through, and if a row on it is now blocked. A stale limit is a lie in the other direction.

4
attacks get past the pack — named below, and pinned by the suite
~ +6 ms
added per tool call — stated as about, never to one decimal
7
checks
16 / 36
vault CLIs, verb rows
20 + 4
vendor patterns, generic shapes
22
protected paths
the four that get through
  • xargsthe operand arrives on stdin at run time, so no static view of the command text holds it
  • a path set by an earlier callthe hook sees one command and holds no model of the shell's state
  • write a reader, then run itthe read happens inside a process no hook is ever shown
  • a literal in a heredoc bodya body is text ABOUT commands — the rule that stops a runbook being read as one

The pack also rewrites a credential-shaped literal into a ${NAME} reference before a file write reaches disk. Of four probe literals, three were rewritten — a GitHub token, an AWS key id, a Slack token — and an invented value matching no vendor pattern was left alone. Every literal in that probe is a decoy. The pack replaces what it recognises and names the shapes it matched. It does not guess.

06install & proof

Dry-run
by default.

The installer prints every command it would run, in order, and changes nothing. --commit is the only thing that makes it act.

the client
git clone https://github.com/nbstr/keyless
cd keyless
cargo install --path .
keyless --version              # proves it is on your PATH

requires rust 1.89 or later · cargo install writes to ~/.cargo/bin, which has to be on your PATH — the --version line is there so you find that out now rather than three commands later

and the uid boundary
cargo build --release            # the installer copies from target/release
./install/install.sh             # prints the plan, changes nothing
sudo ./install/install.sh --commit

It needs sudo exactly once, to create a user. Nothing afterwards does. uninstall.sh reverses all of it — the launchd job, the files, and the account — and is dry-run too.

MIT licensed · github.com/nbstr/keyless

the seven verbs

run · ls · items · fields · new · put · doctor

and the two that do not exist
$ keyless get FOO
error: unrecognized subcommand 'get'

$ keyless put FOO --value hunter2
error: unexpected argument '--value' found

both observed live — the absence is the design

24
packages in the lock file
464
tests, 0 failed, 15 ignored
0
values printed, ever
observed, not quoted
  • cargo testexit 0 — 464 passed, 0 failed, 15 ignored
  • cargo fmt --checkexit 0 — zero bytes out
  • python3 hooks/tests/run.py613 checks, all green
  • python3 hooks/tests/mutate.py41 / 41 mutations caught
  • CI at this sha6 jobs, 1m29s, clippy -D warnings on both runners

There is no telemetry, and a test scans the binary for endpoint strings to keep it that way.