From c42312962c38bd56072e7123f70977c48a2e8306 Mon Sep 17 00:00:00 2001 From: "gominimal-aw-bot[bot]" <281738952+gominimal-aw-bot[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:36:19 +0000 Subject: [PATCH 1/2] chore(minimal): hide `min session attach --command` from help Add `hide = true` to the `#[arg(long, short)]` attribute on `AttachArgs::command` so `-c` / `--command` no longer appear in `min session attach --help`, `-h`, or shell completions. The flag stays functional for existing scripted callers; the option promises a general remote exec it cannot deliver (no PTY, only three daemon commands accepted), so it should not be advertised for discovery. The non-TTY attach error still points stuck callers at `--command`; reword it to frame the flag as a deliberate hidden escape hatch rather than drop the reference, keeping the advice actionable now that the flag is off the help surface. --- crates/minimal/src/lib.rs | 60 ++++++--------------------------------- 1 file changed, 9 insertions(+), 51 deletions(-) diff --git a/crates/minimal/src/lib.rs b/crates/minimal/src/lib.rs index c698a60d3..cf58148e8 100644 --- a/crates/minimal/src/lib.rs +++ b/crates/minimal/src/lib.rs @@ -443,7 +443,7 @@ pub struct AttachArgs { #[arg(add = completion::session_completer())] pub session: Option, /// Command to exec in the session context (non-interactive) - #[arg(long, short)] + #[arg(long, short, hide = true)] pub command: Option, } @@ -492,10 +492,6 @@ pub struct InitArgs { /// Skip confirmation, writing configuration based on auto-detection #[arg(long, short, default_value_t = false)] pub yes: bool, - /// Overwrite an existing minimal.toml; without it, init refuses when one - /// already exists. - #[arg(long, default_value_t = false)] - pub force: bool, } #[derive(Debug, Args)] @@ -1362,7 +1358,7 @@ fn offer_mfile_scaffold( .build() .map_err(|e| anyhow::anyhow!("{e}"))? }; - run_init_flow(config, false, false) + run_init_flow(config, false) } /// Resolves the directory whose tree should be uploaded as the session @@ -1865,8 +1861,9 @@ fn ensure_interactive_attach_tty(stdin_is_tty: bool) -> Result<(), anyhow::Error } else { bail!( "`min session attach` needs an interactive terminal, but stdin is not a TTY. \ - Run it from a terminal, or use `min session attach --command ` to run a \ - single command non-interactively." + Run it from a terminal, or use the non-interactive escape hatch \ + `min session attach --command ` (a hidden flag, kept for scripted \ + callers) to run a single command." ) } } @@ -1896,27 +1893,9 @@ async fn attach_to_session( let [strict, known_hosts_file] = host_key_opts(&sock.with_file_name(paths::KNOWN_HOSTS_FILE)); let mut ssh = std::process::Command::new("ssh"); - // Pin the shell ssh uses to run the ProxyCommand. ssh launches a - // ProxyCommand via `$SHELL -c` and execs `$SHELL` with no PATH lookup, so a - // caller whose `$SHELL` is a bare name (`fish`) or points at a shell absent - // from this context fails with ": No such file or directory" and the - // transport dies at "banner exchange … Broken pipe". Our ProxyCommand is a - // full-path `min proxy …` that needs nothing but a POSIX `sh`, so force the - // always-present `/bin/sh` rather than inherit the user's interactive shell. - ssh.env("SHELL", "/bin/sh"); ssh.env("MINIMAL_SESSION_ID", id.to_string()).args([ "-o", "SendEnv=MINIMAL_SESSION_ID", - // Forward the user's locale and timezone into the session, mirroring a - // conventional `SendEnv LANG LC_* TZ`. The daemon accepts only these - // (its `AcceptEnv` allowlist) and folds them in below any loadout. - // `TERM` needs no `SendEnv`: ssh always carries it in the PTY request. - "-o", - "SendEnv=LANG", - "-o", - "SendEnv=LC_*", - "-o", - "SendEnv=TZ", "-o", &format!("ProxyCommand={proxy_cmd}"), "-o", @@ -2552,32 +2531,15 @@ pub fn build_config(global: &GlobalArgs) -> Result { /// generate a `minimal.toml`, show the plan, prompt for confirmation, /// and write the file. Shared by `cmd_init` and the `cmd_activate` /// missing-mfile prompt. -fn run_init_flow( - config: mctx::Config, - skip_confirm: bool, - force: bool, -) -> Result<(), anyhow::Error> { +fn run_init_flow(config: mctx::Config, skip_confirm: bool) -> Result<(), anyhow::Error> { use op::ProjectOp as _; let mut env = mctx::ProjectSetup::for_init(config).map_err(|e| anyhow::anyhow!("{e}"))?; let plan = op::InitProject .run(&mut env) .map_err(|e| anyhow::anyhow!("{e}"))?; - // Overwriting an existing minimal.toml is destructive — no backup is - // written — so require an explicit --force rather than let confirm() read - // a non-TTY EOF as a silent "yes". Mirrors `min session destroy --all`, - // which likewise refuses non-interactively and names the flag to proceed. - let exists = plan.toml_path.exists(); - if exists && !force { - bail!( - "refusing to overwrite existing {} without confirmation; pass --force", - plan.toml_path.display() - ); - } - if !skip_confirm { - let verb = if exists { "overwrite" } else { "create" }; - eprintln!("\nWill {verb} {}:\n", plan.toml_path.display()); + eprintln!("\nWill create {}:\n", plan.toml_path.display()); eprintln!("---"); eprint!("{}", plan.content); eprintln!("---"); @@ -2591,11 +2553,7 @@ fn run_init_flow( std::fs::write(&plan.toml_path, &plan.content) .with_context(|| format!("writing {}", plan.toml_path.display()))?; - eprintln!( - "{} {}", - if exists { "Updated" } else { "Created" }, - plan.toml_path.display() - ); + eprintln!("Created {}", plan.toml_path.display()); Ok(()) } @@ -2603,7 +2561,7 @@ fn run_init_flow( /// Initialize a `minimal.toml` based on the source tree. pub async fn cmd_init(global: &GlobalArgs, args: InitArgs) -> Result<(), mctx::Error> { let config = build_config(global)?; - run_init_flow(config, args.yes, args.force).map_err(mctx::Error::Other) + run_init_flow(config, args.yes).map_err(mctx::Error::Other) } /// Add packages as dependencies to the project's `minimal.toml`. From d681e4d2d2227656663c8145bb6c1dcb0d0c8146 Mon Sep 17 00:00:00 2001 From: Norrie Taylor Date: Wed, 29 Jul 2026 14:04:15 -0700 Subject: [PATCH 2/2] fix(minimal): restore #1053 and #1060 clobbered by the merge commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge commit c0f9a4f resolved conflicts in crates/minimal/src/lib.rs by taking the pre-merge side, silently reverting two fixes that had landed on main: - #1053 — the `min init` refuse-to-overwrite guard: `InitArgs::force`, the bail on an existing `minimal.toml`, and the Updated/Created wording. Without it `min init` overwrites an existing `minimal.toml` with no backup and no prompt, which is the data loss #1032 was filed for. - #1060 — the `SHELL=/bin/sh` pin on the ssh ProxyCommand and the `SendEnv` forwarding of LANG/LC_*/TZ. Without the pin, attach dies at "banner exchange ... Broken pipe" for any user whose $SHELL is a bare name (fish) or absent from the ssh context. Neither revert was caught by the suite: there is no test for the init guard, and `interactive_attach_requires_a_tty_on_stdin` only asserts the error contains "not a TTY" and "--command", which holds either way. Rebuilt lib.rs from main and reapplied only the intended change, so the diff against main is now exactly the `hide = true` on `AttachArgs::command` plus the non-TTY error reword: +4/-3, was +9/-51. Verified: rustfmt clean; `cargo build -p minimal --locked` ok; `min session attach --help` no longer lists `-c`/`--command` while `--command` still parses; `min init --help` lists `--force` again. `cargo test -p minimal` cannot run on macOS (dev-deps pull minimald -> procfs/caps, Linux-only), so the suite is left to CI. Co-Authored-By: Claude Opus 5 (1M context) --- crates/minimal/src/lib.rs | 53 +++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/crates/minimal/src/lib.rs b/crates/minimal/src/lib.rs index c9ac4e255..fdb43285b 100644 --- a/crates/minimal/src/lib.rs +++ b/crates/minimal/src/lib.rs @@ -492,6 +492,10 @@ pub struct InitArgs { /// Skip confirmation, writing configuration based on auto-detection #[arg(long, short, default_value_t = false)] pub yes: bool, + /// Overwrite an existing minimal.toml; without it, init refuses when one + /// already exists. + #[arg(long, default_value_t = false)] + pub force: bool, } #[derive(Debug, Args)] @@ -1358,7 +1362,7 @@ fn offer_mfile_scaffold( .build() .map_err(|e| anyhow::anyhow!("{e}"))? }; - run_init_flow(config, false) + run_init_flow(config, false, false) } /// Resolves the directory whose tree should be uploaded as the session @@ -1893,9 +1897,27 @@ async fn attach_to_session( let [strict, known_hosts_file] = host_key_opts(&sock.with_file_name(paths::KNOWN_HOSTS_FILE)); let mut ssh = std::process::Command::new("ssh"); + // Pin the shell ssh uses to run the ProxyCommand. ssh launches a + // ProxyCommand via `$SHELL -c` and execs `$SHELL` with no PATH lookup, so a + // caller whose `$SHELL` is a bare name (`fish`) or points at a shell absent + // from this context fails with ": No such file or directory" and the + // transport dies at "banner exchange … Broken pipe". Our ProxyCommand is a + // full-path `min proxy …` that needs nothing but a POSIX `sh`, so force the + // always-present `/bin/sh` rather than inherit the user's interactive shell. + ssh.env("SHELL", "/bin/sh"); ssh.env("MINIMAL_SESSION_ID", id.to_string()).args([ "-o", "SendEnv=MINIMAL_SESSION_ID", + // Forward the user's locale and timezone into the session, mirroring a + // conventional `SendEnv LANG LC_* TZ`. The daemon accepts only these + // (its `AcceptEnv` allowlist) and folds them in below any loadout. + // `TERM` needs no `SendEnv`: ssh always carries it in the PTY request. + "-o", + "SendEnv=LANG", + "-o", + "SendEnv=LC_*", + "-o", + "SendEnv=TZ", "-o", &format!("ProxyCommand={proxy_cmd}"), "-o", @@ -2531,15 +2553,32 @@ pub fn build_config(global: &GlobalArgs) -> Result { /// generate a `minimal.toml`, show the plan, prompt for confirmation, /// and write the file. Shared by `cmd_init` and the `cmd_activate` /// missing-mfile prompt. -fn run_init_flow(config: mctx::Config, skip_confirm: bool) -> Result<(), anyhow::Error> { +fn run_init_flow( + config: mctx::Config, + skip_confirm: bool, + force: bool, +) -> Result<(), anyhow::Error> { use op::ProjectOp as _; let mut env = mctx::ProjectSetup::for_init(config).map_err(|e| anyhow::anyhow!("{e}"))?; let plan = op::InitProject .run(&mut env) .map_err(|e| anyhow::anyhow!("{e}"))?; + // Overwriting an existing minimal.toml is destructive — no backup is + // written — so require an explicit --force rather than let confirm() read + // a non-TTY EOF as a silent "yes". Mirrors `min session destroy --all`, + // which likewise refuses non-interactively and names the flag to proceed. + let exists = plan.toml_path.exists(); + if exists && !force { + bail!( + "refusing to overwrite existing {} without confirmation; pass --force", + plan.toml_path.display() + ); + } + if !skip_confirm { - eprintln!("\nWill create {}:\n", plan.toml_path.display()); + let verb = if exists { "overwrite" } else { "create" }; + eprintln!("\nWill {verb} {}:\n", plan.toml_path.display()); eprintln!("---"); eprint!("{}", plan.content); eprintln!("---"); @@ -2553,7 +2592,11 @@ fn run_init_flow(config: mctx::Config, skip_confirm: bool) -> Result<(), anyhow: std::fs::write(&plan.toml_path, &plan.content) .with_context(|| format!("writing {}", plan.toml_path.display()))?; - eprintln!("Created {}", plan.toml_path.display()); + eprintln!( + "{} {}", + if exists { "Updated" } else { "Created" }, + plan.toml_path.display() + ); Ok(()) } @@ -2561,7 +2604,7 @@ fn run_init_flow(config: mctx::Config, skip_confirm: bool) -> Result<(), anyhow: /// Initialize a `minimal.toml` based on the source tree. pub async fn cmd_init(global: &GlobalArgs, args: InitArgs) -> Result<(), mctx::Error> { let config = build_config(global)?; - run_init_flow(config, args.yes).map_err(mctx::Error::Other) + run_init_flow(config, args.yes, args.force).map_err(mctx::Error::Other) } /// Add packages as dependencies to the project's `minimal.toml`.