agentic/code/frameworks/security-engineering/skills/secret-handling-runtime/SKILL.md
Decision aid for runtime secret hygiene — fd passing, scratch surface, error-path safety, identifier hygiene, and avoiding the SECRETS_ENV aggregation anti-pattern.
npx skillsauth add jmagly/aiwg secret-handling-runtimeInstall 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.
Decision aid for how a system handles secrets during operation: in memory, in pipes, in scratch files, in error messages, in logs, in process tables. Use when designing or reviewing any code that touches secret material at runtime.
This skill complements the existing addons/security/secure-token-load.md (which covers tokens at rest — file modes, heredoc patterns, source locations). This skill covers runtime — what happens to those secrets between load and use, and what happens when something fails mid-operation.
# Caller
encrypt_program 3<<<"$SECRET_VALUE" 4<<<"$OTHER_SECRET" arg1 arg2
# Inside encrypt_program
secret=$(cat <&3)
other=$(cat <&4)
# fd 3 and fd 4 are open for the duration of the program
# never appear in /proc/self/environ, never in `ps aux`
Why this beats env vars:
env, printenv, /proc/<pid>/environ are readable by other processes on most systemsexec chains (subprocess inherits)set -x (bash trace) prints env values for each subprocess invocation# WRONG
SECRETS_ENV="api_key=$KEY1;db_pwd=$KEY2;jwt_secret=$KEY3"
some_program # all three secrets visible to any process inspecting environ
Single variable = single leak vector. If set -x fires, all three leak. If a child process logs env, all three leak. Use one fd per secret OR one named pipe per secret.
mkfifo /tmp/secrets.fifo
chmod 600 /tmp/secrets.fifo
{ echo "$SECRET" > /tmp/secrets.fifo; } &
program /tmp/secrets.fifo
rm /tmp/secrets.fifo
Caveats:
/tmp/ should be tmpfs (Section 2)ls -la /tmp/; use a per-process directory in /run/<uid>/ or /dev/shm/ that's mode 700/tmp is not tmpfsrequire_tmpfs() {
local path="${1:-/tmp}"
if ! mountpoint -q "$path" || \
[ "$(findmnt -no FSTYPE "$path")" != "tmpfs" ]; then
echo "ERROR: $path is not tmpfs; refusing to operate" >&2
exit 1
fi
}
require_tmpfs /tmp
Why tmpfs only:
/tmp (some servers, some embedded systems) leaves traces in flash wear-leveling, journal, and swapshred -u on flash storage is worse than nothing — it gives false assurance against wear-leveling reality (review M1)shred is not a substituteshred overwrites file content multiple times then deletes. On flash storage (SSD, USB, eMMC), the wear-leveling layer remaps writes to fresh blocks; the original data sits in unmapped blocks until eventual reuse. shred -u deletes the visible file but the data remains recoverable via filesystem forensics.
shred IS appropriate for spinning disks where the OS guarantees in-place rewrites — uncommon in 2026.
For ephemeral scratch, tmpfs is the answer. For persistent encrypted storage, the encryption (LUKS, FileVault) is the answer; never write secrets to "ordinary" disk and try to delete them later.
## Scratch surface
| Path | Backing | Acceptable for secrets? |
|------|---------|-------------------------|
| /tmp | tmpfs (verified at start) | yes |
| /var/tmp | disk | NO — refuse to write secrets |
| /dev/shm | tmpfs | yes |
| ~/.cache | disk | NO |
set -euo pipefail + trap cleanup#!/bin/bash
set -euo pipefail
cleanup() {
# zeroize what we can; unmount tmpfs scratch
[ -n "${SCRATCH_DIR:-}" ] && [ -d "$SCRATCH_DIR" ] && \
shred -u "$SCRATCH_DIR"/*.tmp 2>/dev/null
[ -n "${SECRET_VAR:-}" ] && unset SECRET_VAR
[ -n "${MOUNTED:-}" ] && umount "$MOUNTED" 2>/dev/null
}
trap cleanup ERR EXIT INT TERM
# ... actual script logic
Without set -e, a failing openssl enc or cat mid-pipeline can produce a partial-state file the next step processes (review M2). Without the trap, an interrupted script leaves secret-bearing scratch files intact.
| Issue | Mitigation |
|---|---|
| set -e doesn't fire inside if/while/&&/\|\| chains | Test exit codes explicitly in those contexts: if foo; then ...; else echo "foo failed"; exit 1; fi |
| set -e ignores failures in subshells unless caller checks | (...) returns the subshell's exit; check it |
| set -x (debug) prints expanded variables — including secrets | Never enable set -x on secret-bearing code paths; use set +x defensively before secret handling |
| cat /etc/secret \| openssl enc ... — pipe failures partial-write | set -o pipefail to make the pipeline return any failure |
| Language | Pattern |
|---|---|
| Python | try/finally for cleanup; subprocess.run(..., check=True); secrets.compare_digest for any constant-time compare |
| Node | try/finally + process.on('exit', cleanup); pass-fd via child_process.spawn(..., { stdio: [...] }) |
| Go | defer cleanup(); os/exec.Cmd.ExtraFiles for fd passing |
In long-running processes (daemons, sessions), wipe key material from memory after use:
// C with libsodium
sodium_memzero(key, sizeof(key));
# Python — limited; CPython doesn't guarantee zeroization but bytearrays help
import ctypes
key = bytearray(32)
# ... use key
ctypes.memset(ctypes.addressof(ctypes.c_char.from_buffer(key)), 0, len(key))
For short-lived programs (one-shot scripts), explicit zeroization is mostly performative — the process exits, OS reclaims memory. The bigger concern is:
ps aux reveals them)mlock to prevent swapmlock(key_buffer, key_size);
// ... use
sodium_memzero(key_buffer, key_size);
munlock(key_buffer, key_size);
mlock prevents the page from being swapped to disk. Required when the process may run on a system with swap enabled. libsodium's sodium_mlock/sodium_munlock is portable.
{
"yk5_serial": "12345678",
"bio_serial": "87654321"
}
Hardware serials in metadata expose:
.meta.json learns what other devices the operator carries)import hashlib, secrets
salt = secrets.token_bytes(16) # per-USB, stored alongside
yk5_hash = hashlib.sha256(salt + yk5_serial.encode()).hexdigest()
# .meta.json stores yk5_hash and salt; serial is never persisted
The salt is stored with the metadata, so loss of the metadata file reveals nothing. The hash lets you verify "is this the right key?" without revealing which key.
The pattern applies to:
If you need to identify-without-revealing, salted-hash. If you need to compare-without-revealing, HMAC with a per-context key.
# Provisioning
cryptsetup luksHeaderBackup /dev/sdX --header-backup-file /secure/headers/sdX.luksheader
gpg --encrypt --recipient hq-pubkey /secure/headers/sdX.luksheader
# Store encrypted backup off-device
# Recovery (when header is corrupted)
cryptsetup luksHeaderRestore /dev/sdX --header-backup-file /secure/headers/sdX.luksheader
A single bad block in the LUKS header bricks the volume. Backups are non-negotiable for production. The backup is encrypted to an HQ key (not the operator's keys) so its loss doesn't compromise active sessions and its theft doesn't compromise active sessions either — restoration requires HQ.
# WRONG
log "Attempting to decrypt with key prefix: ${KEY:0:4}..."
# RIGHT
log "Decrypt attempt: kid=$(hash_id $KEY_NAME) result=$result"
Truncated keys are still useful to attackers (rainbow table search space reduction; structural fingerprint).
shred on flash storageOriginal: scripts use shred -u to delete secret-bearing files on the USB drive.
What this skill flags:
shred on flash is worse than nothingRemediation:
/tmp is tmpfs at script start; refuse if notset -e not specifiedOriginal: scripts use set -uo pipefail but not -e.
What this skill flags:
-e allows silent partial-state errorsRemediation:
set -euo pipefail to all secret-bearing scriptstrap cleanup ERR EXIT INT TERMOriginal: all credentials concatenated into one SECRETS_ENV variable.
What this skill flags:
Remediation:
export secret-bearing variables; keep them function-local.meta.jsonWhat this skill flags:
Remediation:
(salt, hash) not raw serialWhat this skill flags:
Remediation:
cryptsetup luksHeaderBackup, encrypt to HQ pubkey, store off-device, document recovery procedure, test itWhen invoked as part of a review, produce findings in standard format. When the system is mature, produce a secret-handling-policy.md document covering Sections 1–7 with project-specific decisions.
addons/security/secure-token-load.md (token files, modes, heredoc patterns)degraded-mode-design (cleanup hygiene fires from degraded-mode triggers)physical-threat-modeling (cold-boot, DMA attacks against in-memory secrets)man 7 random — RNG and memory hygienedata-ai
Report which research-corpus radar sidecars are overdue for refresh. Computes staleness (days since last refresh vs the cadence window) for every radar, sorted most-overdue-first. Runs via `aiwg corpus radar-status`.
data-ai
Aggregate research-corpus radar sidecars into a corpus or per-cluster freshness report — totals, overdue count, per-cluster / per-GRADE / per-trajectory breakdowns, an overdue table, and per-radar rationale snippets. Runs via `aiwg corpus radar-report`.
testing
Scaffold radar/freshness sidecars for research-corpus REFs. Pulls title/authors from the citation sidecar and GRADE from the analysis doc, defaults the refresh cadence from GRADE and the cluster from a corpus-local map, and stamps documentation/radar/REF-XXX-radar.md. Runs via `aiwg corpus radar-init`.
data-ai
Compute an entity's publication trajectory — per-year paper counts, topic drift, hot-streak detection (≥3 consecutive A-grade years), and career phase. Runs via `aiwg corpus profile-temporal`.