Keeping credentials out of coding-agent transcripts
A coding agent that operates your own infrastructure will, sooner or later,
grep a token out of a config file and paste it into the session transcript.
This is the design that stopped that on a self-hosted fleet, for two agent
harnesses, after pattern-based detection failed on the exact line that leaked.
It is for anyone running an LLM agent with shell access against machines that
hold real credentials.
Everything below was measured on 2026-09-04 on one developer
workstation (WSL2) plus two Docker hosts and an edge router reached over ssh.
Counts are from the tools’ own output on that day; the scanner comparison is
a single synthetic fixture, not a benchmark. Nothing here was verified on
anyone else’s setup. Rows dated 2026-09-29 were read the same way on the same
workstation: the systemd journal, unit properties, and counts over the live
digest cache with jq. No value, salt or digest was printed; only counts.
- Two leaks in one day, both in the same shape: a value read from a store the agent had a legitimate reason to touch, printed into the transcript, then retyped by the model in its own words. Neither value had a recognisable format.
- On a five-line fixture with labelled secrets, gitleaks (this build has no
generic rule) found 0 of 5, trufflehog found 0 of 5, noseyparker found 2 of
5 and missed the
TOKEN=<48 hex chars>line that leaked. Format and entropy rules cannot tell a token from a commit hash. - What is knowable is where the stores are. A registry lists them; a tool resolves every value in them and hands the guards a keyed HMAC per value, never the value. The guards tokenise text, hash each candidate, and compare.
- Remote stores stay remote: the far host hashes its own container environments and config files and returns digests only. Every running container on a host is one ssh round trip.
- The layers, in the order a leak would have to pass them: block the read of a registered store or of a file containing a registered value; refuse a registered value typed into any tool argument; mask registered values in tool output; mask them in the model’s own message before it is persisted; refuse two or more pieces of a value; treat base64 and percent-encoded spellings as the value.
- On 2026-09-04, on this fleet: 37 local stores and 57 containers on two hosts yield 483 digests in 8 seconds, plus 134 configuration keys excluded so that a timezone string is not masked as a secret.
- On 2026-09-29: 113 stores (42 registry lines) and 49 containers on two hosts yield 402 digests in about 9 seconds, 0 unresolved, with 386 configuration keys excluded.
- On 2026-09-29 the digest cache did not exist: the timer meant to refresh
it could not find the binary under the user service manager (31 runs
logged
command not foundbetween 2026-09-24 and 2026-09-29). The known-value layer failed open wherever no cache existed; the unit reported success and nothing alarmed. The fix sets PATH in the unit and makes an empty pass fail it. - A nightly sweep finds secret-looking files the registry does not cover. Its first run found five real unregistered stores, including the AWS credentials file.
- A value minted during a session is unknown to the registry until its store is registered. Capture it to a file or variable, use it by reference, register the store.
- Since 2026-10-01 vault values are injected per command (
sx NAME -- cmd, reading through rbw) instead of exported into every shell, so an agent process inherits two vault values, not about 22.
How a leak happens
Section titled “How a leak happens”The two incidents that motivated this, in one day:
| Step | Incident 1 | Incident 2 |
|---|---|---|
| Trigger | User asks where a service token lives, adds “do not print it” | Agent prepares a router reflash and checks the wireless config over ssh |
| Read | Agent runs its grep tool over the config directory | Agent runs uci show wireless on the router |
| Exposure | Tool output contains TOKEN=<48 hex chars> | Tool output contains two key='...' lines |
| Amplification | Agent summarises the location, value already in transcript | Agent asks “which key do the plugs hold, <value>?” - retypes it |
| Durable copies | Session log on disk, synced to a searchable store | Same |
Three properties of that table shaped the design. The values had no prefix or
format, so nothing shaped like ghp_ or AKIA applied. The second exposure
was the model’s own prose, which a tool-output filter never sees. And the
read was legitimate: the agent was supposed to look at that config, so a
guard that only asks “is this file secret-shaped” would have had to say yes to
every .env on the machine and still miss the router.
Why patterns fail here
Section titled “Why patterns fail here”The mature scanners all gate a candidate on a nearby keyword and then apply an
entropy or randomness test. gitleaks evaluates keyword, then regex, then
Shannon entropy on the captured group, and its one format-free rule
(generic-api-key) requires a word like key, token or secret within a
short window before the value12.
detect-secrets ships hex and base64 high-entropy plugins with limits of 3.0
and 4.5 bits and a keyword detector that ignores the value
entirely34. ripsecrets runs a randomness test on the
value of an assignment whose name looks secret5. trufflehog treats
entropy as a post-filter and relies on per-provider detectors that verify a
candidate against the provider’s API; unstructured values need a custom
detector6.
Each of those is a reasonable trade for scanning a stranger’s repository. For
an agent reading your own machine they fail in a specific way: the false
positive class is everything else in the transcript. Commit hashes, container
IDs, UUIDs, base64 blobs and minified assets all pass an entropy test; a
40-character hex token and a 40-character git SHA are the same string to a
regex. Tightening the keyword window cuts the false positives and also cuts
the incident above, where the value appeared after a tab in grep output, not
after KEY= in a file.
The CI vendors that hold the line in practice do not pattern-match at all.
GitHub Actions masks the exact values registered with ::add-mask:: or
declared as secrets, plus encoded variants789.
GitLab replaces masked variables by exact substring10. Buildkite
redacts the values of environment variables whose names match a
glob11. 1Password’s op run conceals the values it injected from
stdout and stderr12. All four work from the known values.
The difference for an agent is that the values are not in its process. They are in dotenv files, sops-encrypted files, key files, and the environments of containers on other hosts.
The design
Section titled “The design”In words:
- A registry file lists the stores: local dotenv, sops and key files with
globs, and remote stores as
docker:HOST/*#*(every running container on a host),sshenv:HOST/path#*(a remote env file) oruci:HOST/config#*(an OpenWrt config), and vault items asrbw:NAMEorbw:NAMEwith name globs. On this fleet on 2026-09-29 the registered remote stores are two host-widedocker:lines;sshenv:anduci:are supported and unregistered.excludelines name configuration keys that are never credentials. secretctl digests --jsonresolves every value in every store and emits, per value, a keyed HMAC under a per-run salt, plus digests of its sliding 8-byte windows and of its base64 and percent-encoded spellings. Remote values are hashed on the far host by a shell loop that returnsKEY <bytes> <hex>lines; their plaintext never crosses ssh.- A systemd user timer runs the pass every 10 minutes into a RAM-backed cache file; the guards load that JSON, tokenise any text they see, hash each candidate under the same salt, and look it up. A hit is a registered value.
- A nightly sweep asks the opposite question: which secret-looking files does the registry not cover.
Which layer catches what
Section titled “Which layer catches what”| Situation | Layer | Result |
|---|---|---|
Agent reads a registered store (cat, read, grep) | pre-tool block | denied, reason names the store and the sanctioned commands |
| Agent reads a file that merely contains a registered value (a compose file with a pasted secret, a dump) | pre-tool block | denied as a copy |
| Agent types a registered value into a command, a file body or an edit | pre-tool refusal | denied; the fix is \$VAR or secretctl exec |
A registered value appears in tool output anyway (grep over a directory, docker inspect, curl -v) | output mask | value replaced, label kept |
| The model retypes a value in its own reply or thinking | message mask | masked before the message is persisted or replayed |
| Two or more 8-byte pieces of a value in one command or message | fragment match | refused or masked as an assembly |
| A value base64- or percent-encoded in output or a command | encoded variants | treated as the value |
| A registered value in the harness’s own environment | exact match | masked (this layer predates the registry) |
A plaintext pipeline such as sops -d file | grep KEY or docker exec C env | pre-tool block (primary harness only) | denied; the reason points at secretctl cmp, fp, exec |
What the harness can and cannot do
Section titled “What the harness can and cannot do”The two harnesses differ in what their hooks can change. The primary harness exposes a hook that can replace a tool result and a hook that can replace a finalised message, so every layer above applies. The second harness exposes a pre-tool hook that can deny and a post-tool hook that can only append context, so it gets the deny layers and an alarm that names the label of a value that has already reached the transcript. Deny-before plus alarm-after is the ceiling there until the harness can mutate a result.
Both harnesses share one detection core, so a rule cannot exist in one and
not the other. The second harness’s hook is a fresh process per tool call and
a digests pass takes seconds because of the ssh round trips, so both
harnesses read a 0600 file under \$XDG_RUNTIME_DIR that a systemd user timer
rewrites every 10 minutes (on macOS, under the per-user temp directory that
getconf DARWIN_USER_TEMP_DIR names, rewritten by a launchd agent); the file
holds the salt and digests, no values.
Both guards can also write it, under the same lock, described in the next
section. When no digests are available, the second harness’s hook fails open: the known-value
checks are skipped and the env-dump denial and format alarm still run. Kill
switches: PI_SECRET_GUARD_OFF=1 (both harnesses) and SECRET_GUARD_OFF=1
(the second harness’s hook).
The digest cache and its writers
Section titled “The digest cache and its writers”Three processes can write the cache file. Since 2026-09-29 they share one
protocol: take flock on digests.lock, never unlink it, write to a temp
file, publish by atomic rename, and publish only on exit 0 or 2 (2 = some
store unresolved) with a complete JSON object as output, so a truncated
write never replaces a good cache. A writer that cannot get the lock skips, because the holder is
already refreshing; readers never need the lock.
macOS has no flock(1). Since 2026-10-12 every writer there takes the same
digests.lock with the BSD lockf(1) in file-descriptor mode instead:
lockf -s -t 0 where Linux uses flock -n, -t N for flock -w N, and a
busy lock exits 75 like flock. The launchd agent that stands in for the timer
takes -t 0. The kernel drops the lock when the holder dies, so a crash
leaves no stale lock (measured on macOS 27).
| Writer | When it writes | How it takes digests.lock |
|---|---|---|
| systemd user timer | every 10 min (plus up to 1 min random delay); 2 min after boot | flock -n; a running pass makes the next one skip |
| second harness’s hook, first load | cache missing (a fresh process per tool call) | flock -w 40: waits for an in-flight pass, then reads what it published; runs its own pass only if still nothing exists |
| second harness’s hook, self-heal | cache older than 2x TTL (20 min) | detached flock -n pass that re-checks freshness under the lock |
| primary harness’s hook, self-heal | cache missing or older than 2x TTL | flock -n with the same freshness re-check |
primary harness’s hook, after secretctl set | once the command has returned | flock -w 60, no freshness skip: a timer pass already running may have read the store before the write |
Vault items as stores
Section titled “Vault items as stores”Since 2026-09-23 a vault item is a registrable store: rbw:NAME through the
rbw client, bw:NAME through a local bw serve. One item is one value, its
notes body. A glob such as rbw:PREFIX_* expands through the client’s
name-only listing, so no value is read to decide what matches. Each item is a
separate client round trip: a 12-item glob digested in 3,360 ms, and a glob
over the whole vault would make every refresh take about 6 minutes, so
registering the whole vault is opt-in. rbw: is read-only to secretctl set.
Handing a vault value to a command
Section titled “Handing a vault value to a command”Since 2026-10-01 no vault value sits in an interactive shell’s environment
on the dev box. Before that, shell start exported about 22 vault values from
a one-hour RAM snapshot, so every agent process and every child it spawned
inherited all of them, and the first shell after the snapshot expired paid a
foreground vault read of about 5 s. Now a value enters one process when that
process needs it, through a small wrapper, sx:
sx COMPOSER_API_KEY -- sh -c 'curl -H "X-API-Key: $COMPOSER_API_KEY" https://composer.example/api/v1/stacks'sx CLOUDFLARE_API_TOKEN=CLOUDFLARE_TOKEN -- sh -c 'curl -s https://api.cloudflare.com/client/v4/user/tokens/verify -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN"'sx -l # the environment-name -> vault-item map, names onlyThe single quotes matter: the variable expands in the child, where it exists, not in the calling shell, where it does not. Three components share the work:
- rbw holds the unlocked vault. Its background agent keeps the decrypted keys
in memory, the way ssh-agent holds keys15; with
lock_timeoutset to 86400 s here (the default is 3600) one unlock covers a day of reads, and eachrbw getis a round trip to the agent’s local socket. sxmaps an environment name to a vault item through one table in the shell config (EXA_API_KEYreads the itemEXA_SEARCH_API_KEY;NAME=ITEMnames an item directly), reads the items with concurrentrbw getcalls, exports them in its own process only, and replaces itself with the command. No value goes to stdout or stderr, and nothing outlives the child. Measured 2026-10-01: shell start 0.26-0.29 s,sxwith one item 0.64 s, with five 1.10 s; the agent serialises most of the parallel reads.secretctlhands no values out on this path; it supplies the guard. Every item in thesxmap is also registered as anrbw:store, so a value that an injected command prints anyway is masked in output like any other registered value.secretctl exec SOURCE --as VAR -- cmdis the general form of the same handoff for any scheme (a sops file, a dotenv key, a vault item by full reference);sxis the shorthand for vault items by environment name.
Three commands still receive values without sx at the call site: the two
agent harnesses get the search-API key and the research-stack token, and the
forge CLI gets its token, through shell functions that call sx for them.
With the vault locked those run without the values and print a warning
rather than block.
Things that went wrong on the way
Section titled “Things that went wrong on the way”Each of these is fixed, and each was found by using the tool rather than by reasoning about it.
- The tool-argument layer ran after the message layer. The message hook masked the value inside a pending tool call, the harness executed the masked command, and the tool-argument layer saw nothing to refuse. Fix: the message layer remembers the call id and the tool layer refuses it with the real reason.
- The model routed around the whole-value block. Asked to run a command
containing a value, and refused, it assembled the value at runtime from
8-character chunks with
printf. Fragment digests were added for exactly this. The deny reason now names that assembly as a policy violation. - Fragments of word-shaped values are words. A registered value that was a
comma-separated list of repository names produced windows like
secretctanddotfiles, and the guard refused an ordinary command for mentioning two repositories. Fragments are now emitted only for opaque tokens (letters and digits, no list or URL punctuation) and a piece must be chunk-shaped and fully covered by one value’s windows. - Configuration is not credentials. Container environments hold
TZ,LANG,EMAIL,POSTGRES_DB, hostnames and usernames alongside secrets. Digesting them masked a timezone string and blocked a command for mentioning a database name in a comment. The registry’sexcludedirective removed 134 such keys on this fleet; the failure mode of forgetting one is over-redaction, never a leak. - The refresh herd. Serial per-store resolution made a pass take minutes,
and every guard process re-ran it on its own 10-minute TTL. On 2026-09-23
that reached 130 concurrent passes and a load average of 21. Stores now
resolve on a pool of 8 workers (
--jobs), a solo pass measured 18.6 s, and the guards take a single-flight lock. - A guard that failed open silently. The timer added for the herd fix
was not writing the cache: the user service manager’s PATH was
/usr/local/bin:/usr/binand the binary lives in~/.local/bin. The oldExecStartended in|| rm -f tmp, so when the digests run produced nothing thermsucceeded, the script exited 0, and systemd logged every run as finished. 31 runs in the journal’s 8-day window loggedsecretctl: command not found. Wherever no cache existed, the known-value layer was skipped; the env-dump denial and format alarm kept working. Fixed 2026-09-29 withEnvironment=PATH=in the unit and a publish rule that fails the unit on empty output. A guard that fails open needs a liveness signal of its own; one was added the same day (see the next section). - Identifiers are not credentials either. A vault item that holds an
identifier rather than a credential, registered as if it were a credential,
makes the second harness’s output alarm fire on ordinary output that
contains it. The registry’s
excludedirective matches configuration keys in an environment, and a vault item’s key is its field (notes), so a bare pattern could exclude every vault item or none. Fixed 2026-09-29: a pattern with a scheme prefix, such asexclude rbw:NAME#*, matches the entry’s label instead, which names one item. - A private RAM file for the digests. A per-call hook cannot afford an 8-second resolve, so it reads a stale cache that refreshes in the background, and only the first-ever call pays the full resolve. As of 2026-09-29 a systemd timer is the routine writer; a hook call that finds no cache waits up to 40 s for a pass in flight and pays the synchronous resolve only if nothing was published.
- Three writers, three lock schemes. Until 2026-09-29 the timer flocked
digests.lock; the second harness’s hook created the same path with O_EXCL on first load and never removed it, and its stale refresh flocked a different file,digests.json.lock, so it could overlap a timer pass; the primary harness’s hook used an O_EXCL PID marker ondigests.lock, which could never succeed once the timer’s empty lock file existed, so its self-heal and its forced refresh after a rotation never ran. The forced refresh also fired beforesecretctl sethad run. The second harness’s stale refresh used a 10-minute TTL equal to the timer’s period, so it fired about a minute before the timer’s pass. All three now use the one protocol above, the hooks refresh only past 2x TTL, and the forced refresh fires after the command returns. - The cache directory resolved to a relative path. On 2026-10-06, on a
macOS machine, one guard call wrote its missing-cache marker and then a
freshly resolved
digests.jsonintosecret-guard/inside the repository it was running in, a public repository’s working tree, while the real cache sat untouched in the temp directory. The resolver tookSECRET_GUARD_CACHE_DIR ?? ...;??keeps an empty string, and joining an empty base gives a path relative to the caller’s working directory. The macOS fallback had the same hole whengetconfprinted nothing orTMPDIRwas empty. Which empty value fired that day was not established. For that call the known-value layer reported the cache missing, so it failed open as designed; the directory was kept out of git and deleted. Fixed the same day: a blank or relative base counts as unset, so every resolved directory is absolute, with test cases for each shape.
Reading the numbers
Section titled “Reading the numbers”| Figure | Value | How it was checked |
|---|---|---|
| Local stores registered | 37 files | secretctl sources |
| Containers covered | 57 across two hosts | secretctl digests --json, labels grouped by host |
| Digests emitted | 483 | same JSON, entries length |
| Configuration keys excluded | 134 | same JSON, skipped_excluded |
| Values with fragment digests | 120 | same JSON, opaque tokens only |
| Values with encoded variants | 222 | same JSON |
| Full pass, both hosts | 8.0 s | time secretctl digests --json |
| One host, 8 containers, plus a local sops file | 3.1 s | same, temporary registry |
| Same credential, remote container vs local sops file | identical digest under one salt | digests --json --salt-file, two labels, one hex |
| Fixture scan: gitleaks / trufflehog / noseyparker | 0 / 0 / 2 of 5 lines | each tool on one synthetic five-line file |
| Coverage sweep, first run | 280 flagged, 70 covered, 210 uncovered | secretctl coverage before noise filters |
| Coverage sweep, after filters | 58 flagged, 35 covered, 23 uncovered | same, with vendored code, fixtures, docs and templates ignored |
| Real stores the sweep found unregistered | 5 | manual triage of the first run |
| Stores registered (2026-09-29) | 113 (42 registry lines: sops, keyfile, dotenv, host-wide docker and vault-item stores) | secretctl sources |
| Containers covered (2026-09-29) | 49 across two hosts | same JSON, labels grouped by host |
| Digests emitted (2026-09-29) | 402, unresolved 0 | same JSON, entries and unresolved length |
| Configuration keys excluded (2026-09-29) | 386 (103 patterns) | same JSON, skipped_excluded, exclude length |
| Values skipped as shorter than 8 bytes (2026-09-29) | 249 | same JSON, skipped_short |
| Values with fragment digests (2026-09-29) | 155 (8-byte window) | same JSON, fragments |
| Values with encoded variants (2026-09-29) | 236 | same JSON, variants |
| Full pass via the timer (2026-09-29) | 9.1-9.3 s wall, 13.9-14.1 s CPU, peak 375 MB then 290 MB | systemd properties, journal Consumed lines |
| Timer pass wall time, all 9 passes on 2026-09-29 | 9.1 s to 31.8 s | journal Consumed ... wall clock lines |
Solo pass after --jobs (2026-09-23) | 18.6 s (9.1 s user) | commit message measurement |
| 12-item vault glob (2026-09-23) | 3,360 ms | recorded in the tool’s documentation |
Timer runs logging command not found (8-day window) | 31 of 377, 2026-09-24 09:20 to 2026-09-29 05:37 (+08) | journalctl --user -u secretctl-digests.service --since '-8 days' | rg -c 'command not found' |
| Synthetic verification (2026-09-29) | real cache: store read denied, env dump denied, unregistered file allowed. Canary registry: typed value, base64 spelling and a value split across two chunks all denied; value in tool output raised the alarm; store read denied; unrelated random hex allowed | synthetic tool calls fed to the hook script |
The scanner row rests on one fixture and one build of each tool, and the
gitleaks binary on this machine ships without the generic-api-key rule, so
the row describes that installation rather than gitleaks in general. It is in
the table because it is the observation that ended
the pattern approach.
What it does not catch
Section titled “What it does not catch”- Pieces shorter than 8 characters, or a value that only exists after a shell runs (a variable built from substrings, a decryption the agent triggers). The deny reason says that doing this deliberately is a policy violation; it cannot stop a model that decides to.
- A value embedded mid-way inside a larger base64 blob at a non-zero alignment. The encoded variants cover the value encoded on its own.
- Anything in a store nobody registered. The nightly sweep exists to shrink this, and its output is a triage list that someone still has to act on.
- In the harness that cannot mutate results, a value that reaches output is in the transcript; the alarm is a rotation prompt.
- Remote formats that are not key-value lines.
uci showwas added because it leaked; the next format will need its own extractor. Theuci:scheme exists but nouci:store is registered on this fleet as of 2026-09-29. - A value minted during the session (2026-09-20: a generated forge token echoed into the transcript). The registry cannot know it until its store is registered: capture it to a file or variable, use it by reference, register the store.
- A missing digest cache. A first load waits up to 40 s for a pass in flight; if nothing is published by then, both hooks run without the known-value layer, with no notice.
- A rotation made outside the primary harness. Only that harness forces a
refresh after
secretctl set(waiting up to 60 s for the lock); a value rotated from anywhere else is unknown until the next timer pass, up to about 11 minutes.
Known issues
Section titled “Known issues”Since 2026-09-29 both guards report the cache’s health instead of failing open silently. Each call classifies the cache as ok, stale (older than 2x the 10-minute TTL), missing, or corrupt (not a complete JSON object). On any state but ok, the second harness’s hook adds a one-line warning to the tool call - appended to the reason when it also denies - at most once per 10 minutes per state, tracked by a marker file’s mtime because the hook is a fresh process per call; the primary harness raises the same warning through its existing notice once per state change. The layer still fails open; the warning makes that visible. Checked 2026-09-29 by feeding a synthetic call to the hook with an empty scratch cache directory: the first call carried “known-value layer OFF (cache missing)”, the second within the window carried nothing.
The second harness cancels a command hook that reaches its timeout and lets the tool call through (its documented behaviour), so a hook timeout is a fail-open path too. Its documented default for command hooks is 600 s; since 2026-09-29 the guard’s entries set 150 s, above the worst case of a 40 s first-load wait plus a 90 s pass, so a hung hook cannot stall a call for ten minutes.
Which to pick
Section titled “Which to pick”| You have | Do this |
|---|---|
| An agent harness with a result-mutating hook | all layers; register stores as you create them |
| A harness with deny-only hooks | the deny layers plus the alarm; treat an alarm as “rotate now” |
| Secrets only in the agent’s own environment | the exact-match layer alone is enough |
| Vault secrets the shell exports into every process | stop exporting; inject per command (sx or secretctl exec) and register the vault items |
| Secrets on other hosts | register them as remote stores; never add a fetch-the-plaintext path |
| Container environments that mix config and secrets | register the host, then exclude the configuration keys the first time they mask something |
A pile of .env files of unknown status | run the coverage sweep and register or encrypt what it lists |
| A guard that fails open when its data is missing | give the data source its own liveness check; a unit that exits 0 on an empty pass is dead config |
| A token you just generated | write it to a file or variable, use it by reference, register the store before the next step |
Related docs
Section titled “Related docs”The session store the transcripts sync to, and the reason a leaked value is durable, is described in A cross-client memory store for coding agents. The container hosts whose environments are registered here are the ones laid out in Docker Servarr stack security. The sibling guards over the same hook machinery - provenance for specifics, AI prose tells, smart punctuation, confidential identifiers, mutating git/gh commands, and the stow-link failure that kept one of them from ever loading - are in Guard extensions that enforce prose and process rules in a coding agent.
References
Section titled “References”-
gitleaks, “detect.go,” GitHub. https://github.com/gitleaks/gitleaks/blob/master/detect/detect.go ↩
-
gitleaks, “generic.go (generic-api-key rule),” GitHub. https://github.com/gitleaks/gitleaks/blob/master/cmd/generate/config/rules/generic.go ↩
-
Yelp, “high_entropy_strings.py,” detect-secrets, GitHub. https://github.com/Yelp/detect-secrets/blob/master/detect_secrets/plugins/high_entropy_strings.py ↩
-
Yelp, “keyword.py,” detect-secrets, GitHub. https://github.com/Yelp/detect-secrets/blob/master/detect_secrets/plugins/keyword.py ↩
-
sirwart, “ripsecrets README,” GitHub. https://github.com/sirwart/ripsecrets/blob/main/README.md ↩
-
Truffle Security, “Custom detectors,” trufflehog, GitHub. https://github.com/trufflesecurity/trufflehog/blob/main/pkg/custom_detectors/CUSTOM_DETECTORS.md ↩
-
GitHub, “Workflow commands for GitHub Actions,” GitHub Docs. https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands ↩
-
GitHub, “SecretMasker.cs,” actions/runner, GitHub. https://github.com/actions/runner/blob/main/src/Sdk/DTLogging/Logging/SecretMasker.cs ↩
-
GitHub, “ValueEncoders.cs,” actions/runner, GitHub. https://github.com/actions/runner/blob/main/src/Sdk/DTLogging/Logging/ValueEncoders.cs ↩
-
GitLab, “Mask a CI/CD variable,” GitLab Docs. https://docs.gitlab.com/ci/variables/#mask-a-cicd-variable ↩
-
Buildkite, “redact.go,” buildkite/agent, GitHub. https://github.com/buildkite/agent/blob/main/internal/redact/redact.go ↩
-
1Password, “op run,” 1Password Developer. https://www.1password.dev/cli/reference/commands/run/ ↩
-
Linux man-pages project, “flock(2) - Linux manual page,” man7.org. https://man7.org/linux/man-pages/man2/flock.2.html ↩
-
Linux man-pages project, “open(2) - Linux manual page,” man7.org. https://man7.org/linux/man-pages/man2/open.2.html ↩
-
J. Luehrs, “rbw,” GitHub. https://github.com/doy/rbw ↩