From 1b2e4efa5bdd930a9a8681b0fc7b7c2dab25ee5e Mon Sep 17 00:00:00 2001 From: Mohamed Ajab Date: Fri, 16 Oct 2026 15:23:22 +0100 Subject: [PATCH] docs(cli): explain set input modes and newline handling Signed-off-by: Mohamed Ajab --- crates/cli/src/main.rs | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 04fee8e5..b17f0b6d 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -167,12 +167,15 @@ enum Command { json: bool, }, /// Set a secret. Reads the value from a hidden prompt unless --value/--stdin is given. + #[command( + after_help = "Input modes:\n Without --value or --stdin, enter the value at the hidden prompt.\n --stdin reads until EOF and preserves every byte, including line endings and trailing newlines. It does not trim or interpolate input. A redirected file becomes one complete secret value, not a dotenv import.\n --value and --stdin cannot be combined. Real credentials passed with --value may appear in shell history and process arguments.\n\nBefore piping input:\n Initialise the local store and project, and unlock the local identity first. --stdin replaces only the value prompt; it does not bypass these requirements or grant write access through a machine token.\n\nExamples (POSIX shell):\n sotto set DATABASE_URL\n sotto set PRIVATE_KEY --stdin < ./example-key.txt\n printf '%s' 'example-value' | sotto set EXAMPLE --stdin\n\nThe pipeline uses a synthetic value. printf avoids the extra newline commonly added by echo. Do not put real credentials in an inline shell example: they can still enter shell history." + )] Set { name: String, - /// Provide the value inline (warning: visible in shell history). + /// Provide the value inline (warning: visible in shell history and process arguments). #[arg(long, conflicts_with = "stdin")] value: Option, - /// Read the value from standard input. + /// Read the complete value from standard input until EOF, without trimming. #[arg(long)] stdin: bool, }, @@ -2322,6 +2325,32 @@ mod tests { assert!(help.contains("Sotto options go before --")); } + #[test] + fn set_help_explains_prompt_and_stdin_input() { + let mut command = Cli::command(); + let help = command + .find_subcommand_mut("set") + .expect("set subcommand should exist") + .render_long_help() + .to_string(); + + for expected in [ + "sotto set DATABASE_URL", + "sotto set PRIVATE_KEY --stdin < ./example-key.txt", + "printf '%s' 'example-value' | sotto set EXAMPLE --stdin", + "reads until EOF", + "trailing newlines", + "does not trim or interpolate", + "not a dotenv import", + "--value and --stdin cannot be combined", + "shell history and process arguments", + "unlock the local identity first", + "synthetic value", + ] { + assert!(help.contains(expected), "missing {expected:?} in:\n{help}"); + } + } + #[test] fn import_help_explains_literal_and_quoted_values() { let mut command = Cli::command();