diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -402,6 +402,7 @@ cargo run --release -p lightcraft                       # opens your library (~/
 cargo run --release -p lightcraft -- ~/Pictures/trip    # import your photos (folders are scanned, duplicates skipped)
 cargo run --release -p lightcraft -- --memory           # a throwaway in-memory demo session (writes nothing)
 cargo run --release -p lightcraft -- --control 7980     # with the automation channel
+cargo run --release -p lightcraft -- --no-update-check  # don't look for a newer release at start (Help ▸ Check for Updates… still works)
 cargo xtask web --serve                                 # the same app in the browser: http://127.0.0.1:8080/
 cargo run --release -p lightcraft-cli -- render photo.jpg -o out.jpg --set light.exposure=0.5
 cargo xtask ci                                          # fmt, clippy, HEIF, tests, parity, layers, assets, WASM
diff --git a/apps/lightcraft/src/launch.rs b/apps/lightcraft/src/launch.rs
--- a/apps/lightcraft/src/launch.rs
+++ b/apps/lightcraft/src/launch.rs
@@ -13,6 +13,8 @@ OPTIONS:
   --memory         throwaway in-memory session (alias --demo); nothing is saved
   --control PORT   serve the JSON-lines control channel on 127.0.0.1:PORT (env LIGHTCRAFT_CONTROL_PORT;
                    see docs/control-protocol.md)
+  --no-update-check
+                   don't look for a newer release at start (Help > Check for Updates… still works)
   --version, --help
 
 ENVIRONMENT:
@@ -28,6 +30,50 @@ ENVIRONMENT:
                    <settings folder>/logs/lightcraft.log (previous runs: lightcraft.1.log, lightcraft.2.log)
 ";
 
+/// What the command line asks for (see [`parse_args`]).
+#[derive(Debug, PartialEq)]
+pub(crate) struct LaunchArgs {
+    pub control_port: Option<u16>,
+    /// Files and folders to import, as written (see [`launch_imports`]).
+    pub files: Vec<String>,
+    pub seed_demo: bool,
+    pub in_memory: bool,
+    pub library_dir: Option<std::path::PathBuf>,
+    /// Whether the window looks for a newer release once it is up; `--no-update-check` turns it off.
+    pub startup_update_check: bool,
+}
+
+/// How the command line was read.
+#[derive(Debug, PartialEq)]
+pub(crate) enum Parsed {
+    Run(LaunchArgs),
+    Version,
+    Help,
+    /// An option that doesn't exist.
+    Unknown(String),
+}
+
+/// Reads the command line (without the program name); `control_port` is the one from
+/// `LIGHTCRAFT_CONTROL_PORT`, which `--control` replaces.
+pub(crate) fn parse_args(args: impl IntoIterator<Item = String>, control_port: Option<u16>) -> Parsed {
+    let mut parsed = LaunchArgs { control_port, files: Vec::new(), seed_demo: true, in_memory: false, library_dir: None, startup_update_check: true };
+    let mut args = args.into_iter();
+    while let Some(a) = args.next() {
+        match a.as_str() {
+            "--control" => parsed.control_port = control_port_from("--control", Some(args.next().unwrap_or_default())),
+            "--library" => parsed.library_dir = args.next().map(std::path::PathBuf::from),
+            "--no-demo" => parsed.seed_demo = false,
+            "--memory" | "--demo" => parsed.in_memory = true,
+            "--no-update-check" => parsed.startup_update_check = false,
+            "--version" => return Parsed::Version,
+            "-h" | "--help" => return Parsed::Help,
+            o if o.starts_with("--") => return Parsed::Unknown(a),
+            _ => parsed.files.push(a),
+        }
+    }
+    Parsed::Run(parsed)
+}
+
 /// The launch arguments' files and folders to import (absolute), and a notice for each one that
 /// is not imported (see [`lightcraft_engine::import::launch_path`]; issue #374).
 pub(crate) fn launch_imports(args: &[String]) -> (Vec<String>, Vec<String>) {
diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs
--- a/apps/lightcraft/src/main.rs
+++ b/apps/lightcraft/src/main.rs
@@ -1,6 +1,6 @@
 //! LightCraft desktop app.
 //!
-//! Usage: `lightcraft [--library DIR | --memory] [--no-demo] [--control <port>] [files or folders…]`
+//! Usage: `lightcraft [--library DIR | --memory] [--no-demo] [--no-update-check] [--control <port>] [files or folders…]`
 //!
 //! The library (catalog, presets, thumbnail cache) lives in `--library DIR`, else
 //! `$LIGHTCRAFT_LIBRARY`, else — with Settings ▸ General ▸ At launch set to *Ask me*, or Alt/Option
@@ -55,7 +55,10 @@ use gpu_crash::{gpu_crash_check, gpu_lost_check_at, gpu_lost_marker_path, gpu_lo
 use gpu_crash::{gpu_crash_check_at, gpu_crash_notice, window_wgpu_options};
 #[cfg(test)]
 use launch::launch_imports_in;
-use launch::{HELP, control_port_from, launch_imports, log_dir, panic_log_path_with, startup_failed, stopped_text, stopped_while_running};
+use launch::{
+    HELP, LaunchArgs, Parsed, control_port_from, launch_imports, log_dir, panic_log_path_with, parse_args, startup_failed, stopped_text,
+    stopped_while_running,
+};
 #[cfg(test)]
 use platform::{explorer_select_arg, explorer_select_command, windows_open_url_command};
 use prefs::{PrefsWriter, as_remembered, load_prefs, prefs_path};
@@ -146,33 +149,23 @@ fn main() -> eframe::Result {
         logging::record_panics(logger);
     }
     alloc_release::install();
-    let mut control_port = control_port_from("LIGHTCRAFT_CONTROL_PORT", std::env::var("LIGHTCRAFT_CONTROL_PORT").ok().filter(|v| !v.is_empty()));
-    let mut files = Vec::new();
-    let mut seed_demo = true;
-    let mut in_memory = false;
-    let mut library_dir: Option<std::path::PathBuf> = None;
-    let mut args = std::env::args().skip(1);
-    while let Some(a) = args.next() {
-        match a.as_str() {
-            "--control" => control_port = control_port_from("--control", Some(args.next().unwrap_or_default())),
-            "--library" => library_dir = args.next().map(std::path::PathBuf::from),
-            "--no-demo" => seed_demo = false,
-            "--memory" | "--demo" => in_memory = true,
-            "--version" => {
+    let env_port = control_port_from("LIGHTCRAFT_CONTROL_PORT", std::env::var("LIGHTCRAFT_CONTROL_PORT").ok().filter(|v| !v.is_empty()));
+    let LaunchArgs { control_port, files, seed_demo, in_memory, library_dir, startup_update_check } =
+        match parse_args(std::env::args().skip(1), env_port) {
+            Parsed::Run(args) => args,
+            Parsed::Version => {
                 println!("lightcraft {}", env!("CARGO_PKG_VERSION"));
                 return Ok(());
             }
-            "-h" | "--help" => {
+            Parsed::Help => {
                 print!("{HELP}");
                 return Ok(());
             }
-            o if o.starts_with("--") => {
+            Parsed::Unknown(o) => {
                 eprintln!("lightcraft: unknown option `{o}`\n\n{HELP}");
                 std::process::exit(2);
             }
-            _ => files.push(a),
-        }
-    }
+        };
     // The log file: opened after the arguments, so `--version`, `--help` and a usage error leave
     // no file behind. Records logged until now are written to it first.
     let mut log_file = None;
@@ -267,7 +260,7 @@ fn main() -> eframe::Result {
                 // HDR display: SDR unless the surface, platform and display all allow HDR
                 hdr_display::report(cc.wgpu_render_state.as_ref().map(|r| r.target_format));
                 let mut app = LightcraftApp::new(session, services(cc, app_log_file.as_deref()));
-                app.set_startup_update_check(true);
+                app.set_startup_update_check(startup_update_check);
                 if let Some(ui) = prefs {
                     app.ui = ui;
                 }
diff --git a/apps/lightcraft/src/tests.rs b/apps/lightcraft/src/tests.rs
--- a/apps/lightcraft/src/tests.rs
+++ b/apps/lightcraft/src/tests.rs
@@ -462,3 +462,33 @@ fn on_exit_ends_the_process_on_macos() {
     assert!(stdout.contains("calling on_exit"), "the child didn't run the test: {stdout}");
     assert_eq!(stdout.contains("on_exit returned"), !cfg!(target_os = "macos"), "{stdout}");
 }
+
+fn parsed(args: &[&str]) -> LaunchArgs {
+    match parse_args(args.iter().map(|a| a.to_string()), None) {
+        Parsed::Run(a) => a,
+        other => panic!("expected a launch, got {other:?}"),
+    }
+}
+
+/// `--no-update-check` (throwaway test copies must not call GitHub at start) turns the startup
+/// update check off and nothing else; without it the check stays on.
+#[test]
+fn no_update_check_turns_the_startup_update_check_off() {
+    assert!(parsed(&[]).startup_update_check, "the default is to check");
+    assert!(parsed(&["--memory", "--control", "7981", "Trip"]).startup_update_check, "other options leave it on");
+    let off = parsed(&["--memory", "--no-update-check", "Trip"]);
+    assert!(!off.startup_update_check);
+    // the flag is only that: the other options and the files still read the same
+    assert_eq!(LaunchArgs { startup_update_check: true, ..off }, parsed(&["--memory", "Trip"]));
+    // a value of --library is that folder, never the flag
+    let lib = parsed(&["--library", "--no-update-check"]);
+    assert!(lib.startup_update_check);
+    assert_eq!(lib.library_dir, Some(std::path::PathBuf::from("--no-update-check")));
+}
+
+/// The flag is a known option (an unknown one exits with a usage error) and `--help` lists it.
+#[test]
+fn no_update_check_is_a_known_option_and_listed_in_the_help() {
+    assert_eq!(parse_args(["--no-update-chek".to_string()], None), Parsed::Unknown("--no-update-chek".into()));
+    assert!(HELP.contains("--no-update-check"));
+}
diff --git a/docs/control-protocol.md b/docs/control-protocol.md
--- a/docs/control-protocol.md
+++ b/docs/control-protocol.md
@@ -51,6 +51,9 @@ empty value disables the settings directory, while the emergency panic log retai
 Explicit model-directory environment variables still take precedence over their defaults.
 Also pass `--library DIR` with a separate scratch library: the configuration override does not change
 the default photo-library location. Do not use `--memory` or `LIGHTCRAFT_NO_PREFS` when testing persistence.
+Pass `--no-update-check` as well: the startup check for a newer release (a request to GitHub) is skipped, so a
+throwaway run makes no network request at start, and a newer release can't open the update prompt, which takes the
+keyboard. Help ▸ Check for Updates… (`app.checkForUpdates`) still checks when asked.
 
 A native acceptance run must reject the stderr message `rendering headlessly`: `ui.screenshot`
 automatically uses the CPU fallback after two seconds without a presented frame. That fallback is useful
diff --git a/docs/whats-new.md b/docs/whats-new.md
--- a/docs/whats-new.md
+++ b/docs/whats-new.md
@@ -1,5 +1,12 @@
 # What's new in LightCraft
 
+## 2026-10-27
+
+### `--no-update-check` for throwaway copies
+- Starting LightCraft with `--no-update-check` skips the look for a newer release at start, so a throwaway copy (a test
+  or scripted run) makes no request to GitHub and never opens the update prompt. Help ▸ Check for Updates… still
+  checks when you ask; without the option nothing changes.
+
 ## 2026-10-24
 
 ### Relative-date filters and smart albums follow the clock
