Secrets, Command History, and Destructive-Command Safety
Why a password typed directly on the command line is a real secret-leak risk, how shell history quietly persists commands you've run, and habits that make a genuinely destructive command safer to author and run.
What you'll learn
- Explain at least two specific ways a secret passed directly on the command line can leak
- Explain how shell history persists commands, and why that matters for anything containing a secret
- Apply a checklist of habits that make a genuinely destructive command safer to construct and run
Prerequisites
Explanation
Every real command below runs only in your own terminal — this lesson's exercises model these decisions as data, never executing a real destructive command.
Passing a secret (a password, an API token) directly as a command-line argument — for example, mysql -u admin -pMySecretPassword — is a genuine, well-documented leak risk for two specific, independent reasons: first, on most systems, the full command line of every running process (including its arguments) is visible to other users on the same machine via tools like ps aux, at least briefly while the process runs; second, that exact command — secret included — is very likely to be saved into your shell's history file (commonly ~/.bash_history), persisting in plain text on disk long after the command finished, readable by anyone who later gains access to that file or that account. The safer alternatives are consistent: read a secret from an environment variable the process reads directly (never echoed or logged), from a dedicated secrets file with restricted permissions that the tool reads itself, or via an interactive, non-echoed prompt — never as a plain, visible command-line argument.
Shell history exists specifically to make repeating past commands easy, but that convenience is exactly what makes it a real secrets-hygiene concern: anything typed at a prompt is a candidate for persistence, not just commands you intended to keep. A command prefixed with a leading space is, in many common shell configurations (with HISTCONTROL=ignorespace or ignoreboth set), excluded from being saved to history — a real, deliberate, well-known technique for the rare case a secret genuinely must be typed directly at a prompt. But this is a narrow, fragile safety net (it depends on shell configuration you may not control), not a substitute for using an environment variable, a secrets file, or a prompt in the first place.
For a genuinely destructive command — one that deletes, overwrites, or otherwise cannot be undone — a small set of habits meaningfully reduces the real risk of a costly mistake: run pwd first to confirm your actual location before anything path-dependent; for a recursive delete, run the equivalent ls or find first to see exactly what would be affected before adding -rf; prefer an absolute, fully-typed path over a shell-expanded wildcard you haven't first previewed; and be especially deliberate about a path built from a variable — an unset or empty variable inside a path like rm -rf "$TARGET_DIR"/ can silently collapse to a dangerously broad path if $TARGET_DIR was empty, which is exactly the kind of failure set -u (covered earlier in this course) is designed to catch before it happens.
Example
Modeling why a secret as a bare CLI argument leaks, and a defensive check before a variable-built destructive path is used, as data.
function secretLeaksVia(method) {
// Models the two real, independent leak vectors for a secret passed as a bare command-line argument.
const leakVectors = {
"cli-argument": ["visible to other users via ps", "likely saved into shell history"],
"env-variable": [], // not visible in ps argument lists, not saved into shell history
"secrets-file": [], // not visible in ps argument lists, not saved into shell history
};
return leakVectors[method] ?? [];
}
console.log(secretLeaksVia("cli-argument")); // two real, independent leak vectors
console.log(secretLeaksVia("env-variable")); // none of those two specific vectors apply
function isSafeToUseAsDeletePath(variableValue) {
// A destructive path built from a variable that's empty or unset can silently collapse to something far broader.
return typeof variableValue === "string" && variableValue.trim().length > 0;
}
console.log(isSafeToUseAsDeletePath("/home/user/scratch/old-build")); // true -- a real, specific, non-empty path
console.log(isSafeToUseAsDeletePath("")); // false -- an empty variable would collapse the path dangerouslyTry it yourself
Call isSafeToUseAsDeletePath with undefined (models an unset variable), and confirm it's correctly rejected as unsafe.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
This models choosing a safe way to supply a secret to a command only -- no real secret is ever created or transmitted. Write chooseSecretDelivery(secretIsAlreadyInEnv, hasSecretsFile): if secretIsAlreadyInEnv, return 'env-variable'. Else if hasSecretsFile, return 'secrets-file'. Else return 'interactive-prompt' -- never 'cli-argument'.
Checks: prefers an existing environment variable · falls back to a secrets file · falls back to an interactive prompt as the last resort
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
This models a pre-flight safety check for a destructive command's target path only -- no real deletion occurs. Write isSafeDeleteTarget(path, allowedRoot): return true only if path is a non-empty string, starts with allowedRoot, and is strictly longer than allowedRoot (rejecting allowedRoot itself, to avoid deleting the root of allowed operations entirely).
Checks: accepts a genuine, specific path under the allowed root · rejects a path outside the allowed root entirely · rejects the allowed root itself, not just paths clearly outside it
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Passing a password or API token directly as a command-line argument -- it's typically visible to other users via `ps aux` while the process runs, and is very likely saved in plain text into shell history.
- Assuming shell history only contains commands you deliberately wanted to keep -- by default, virtually everything typed at a prompt is a candidate for persistence, including anything containing a secret typed by mistake.
- Running a destructive command (especially one built from a variable) without first previewing exactly what it would affect -- an unset or empty variable inside a path can silently collapse a recursive delete to a far broader, unintended target.
Knowledge check
Takeaway
Never pass a secret as a bare command-line argument -- use an environment variable, a restricted-permission secrets file, or an interactive, non-echoed prompt instead. Remember shell history persists nearly everything typed at a prompt by default. Before running a genuinely destructive command, especially one built from a variable, preview exactly what it would affect first.
Summary
A secret passed as a command-line argument is visible to other users via ps and is very likely saved into shell history in plain text -- environment variables, secrets files, and interactive prompts are the safer alternatives. Shell history persists nearly everything by default; a leading space (with the right HISTCONTROL setting) is a narrow exception, not a substitute for good habits. A destructive command, especially one built from a variable, deserves a pwd check and a preview of exactly what it would affect before it runs.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.