diff --git a/apps/artcraft-launcher/src/settings_file.rs b/apps/artcraft-launcher/src/settings_file.rs
--- a/apps/artcraft-launcher/src/settings_file.rs
+++ b/apps/artcraft-launcher/src/settings_file.rs
@@ -110,7 +110,8 @@ mod tests {
     #[test]
     fn settings_saved_with_no_file_read_back() {
         let dir = Scratch::new("settings-round-trip");
-        let settings = Settings { last_check: Some(1_791_500_000), notify: false, ..Settings::default() };
+        let mut settings = Settings { last_check: Some(1_791_500_000), notify: false, ..Settings::default() };
+        settings.launch_flags.insert("photocraft".into(), r#"--dir "My Photos""#.into());
         assert_eq!(save(&dir.file(), "settings", &settings), Ok(()));
         assert_eq!(load::<Settings>(&dir.file(), "settings"), Some(settings));
     }
diff --git a/crates/core/src/args.rs b/crates/core/src/args.rs
new file mode 100644
--- /dev/null
+++ b/crates/core/src/args.rs
@@ -0,0 +1,78 @@
+//! The extra command-line flags a user gives an app, from the text they typed.
+
+/// The flags open a quote they never close.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct UnclosedQuote;
+
+/// Splits `text` into arguments: on whitespace, with `"…"` or `'…'` keeping spaces inside one
+/// argument (`--name "My Projects"`). A backslash is an ordinary character, so Windows paths
+/// (`C:\Users\me`) come through as typed. An unclosed quote is an error, not a guess.
+pub fn split(text: &str) -> Result<Vec<String>, UnclosedQuote> {
+    let mut args = Vec::new();
+    let mut current = String::new();
+    // Whether `current` is an argument, even an empty one (`""`).
+    let mut started = false;
+    let mut quote: Option<char> = None;
+    for c in text.chars() {
+        match quote {
+            Some(q) if c == q => quote = None,
+            Some(_) => current.push(c),
+            None if c == '"' || c == '\'' => {
+                quote = Some(c);
+                started = true;
+            }
+            None if c.is_whitespace() => {
+                if started {
+                    args.push(std::mem::take(&mut current));
+                    started = false;
+                }
+            }
+            None => {
+                current.push(c);
+                started = true;
+            }
+        }
+    }
+    if quote.is_some() {
+        return Err(UnclosedQuote);
+    }
+    if started {
+        args.push(current);
+    }
+    Ok(args)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn ok(text: &str) -> Vec<String> {
+        split(text).unwrap()
+    }
+
+    #[test]
+    fn whitespace_separates_arguments() {
+        assert_eq!(ok("  --verbose   -x\t1 "), ["--verbose", "-x", "1"]);
+        assert!(ok("").is_empty());
+        assert!(ok("   ").is_empty());
+    }
+
+    #[test]
+    fn quotes_keep_spaces_and_join_with_what_touches_them() {
+        assert_eq!(ok(r#"--dir "My Projects" 'a b'"#), ["--dir", "My Projects", "a b"]);
+        assert_eq!(ok(r#"--name="A B""#), ["--name=A B"]);
+        assert_eq!(ok(r#""it's" 'say "hi"'"#), ["it's", r#"say "hi""#]);
+        assert_eq!(ok(r#""" x"#), ["", "x"]);
+    }
+
+    #[test]
+    fn backslashes_are_kept_for_windows_paths() {
+        assert_eq!(ok(r"--open C:\Users\me\file.txt"), ["--open", r"C:\Users\me\file.txt"]);
+    }
+
+    #[test]
+    fn an_unclosed_quote_is_an_error() {
+        assert_eq!(split(r#"--dir "My Projects"#), Err(UnclosedQuote));
+        assert_eq!(split("'"), Err(UnclosedQuote));
+    }
+}
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -6,6 +6,7 @@
 
 #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
 
+pub mod args;
 pub mod catalog;
 pub mod details;
 pub mod distro;
diff --git a/crates/core/src/protocol.rs b/crates/core/src/protocol.rs
--- a/crates/core/src/protocol.rs
+++ b/crates/core/src/protocol.rs
@@ -32,8 +32,10 @@ pub enum Command {
     Uninstall {
         app: String,
     },
+    /// Start an installed app, passing `args` (the user's launch flags) on its command line.
     Launch {
         app: String,
+        args: Vec<String>,
     },
     /// Show the installed app in the file manager.
     Reveal {
diff --git a/crates/native/src/backend.rs b/crates/native/src/backend.rs
--- a/crates/native/src/backend.rs
+++ b/crates/native/src/backend.rs
@@ -1063,10 +1063,10 @@ impl Backend for NativeBackend {
                     }
                 }
             }
-            Command::Launch { app } => {
+            Command::Launch { app, args } => {
                 let Some(def) = CraftApp::by_id(&app) else { return };
                 let result = match self.installed(&app) {
-                    Some(inst) => install::launch(def, &inst, &self.paths),
+                    Some(inst) => install::launch(def, &inst, &self.paths, &args),
                     None => Err(format!("{} isn't installed", def.name)),
                 };
                 if let Err(e) = result {
diff --git a/crates/native/src/install.rs b/crates/native/src/install.rs
--- a/crates/native/src/install.rs
+++ b/crates/native/src/install.rs
@@ -537,17 +537,18 @@ fn remove_appimage(app: &CraftApp, path: &Path, record: Option<&Record>) -> Resu
     Ok(())
 }
 
-/// Starts an installed app.
-pub fn launch(app: &CraftApp, installed: &Installed, paths: &Paths) -> Result<(), String> {
+/// Starts an installed app with `args` (the user's launch flags) on its command line.
+pub fn launch(app: &CraftApp, installed: &Installed, paths: &Paths, args: &[String]) -> Result<(), String> {
     let _ = paths;
     let path = Path::new(&installed.location);
     match installed.source {
         InstallSource::Flatpak => {
             let mut cmd = process::flatpak_command();
-            cmd.args(["run", &app.bundle_id()]);
+            // After the app's id: flags before it would go to `flatpak run` itself.
+            cmd.args(["run", &app.bundle_id()]).args(args);
             process::spawn_detached(cmd)
         }
-        InstallSource::MacApp => process::open(path.as_os_str()),
+        InstallSource::MacApp => process::open_app(path, args),
         // A tarball the launcher unpacked into its own folder: run it here, against its own
         // home, rather than on the host through `flatpak-spawn` (which is what `launch_binary`
         // would do inside the sandbox). Its settings then land beside it instead of in the
@@ -557,6 +558,7 @@ pub fn launch(app: &CraftApp, installed: &Installed, paths: &Paths) -> Result<()
             let home = paths.app_home(app.id);
             prepare_home(&home);
             let mut cmd = process::command(path);
+            cmd.args(args);
             for (name, value) in home_env(&home) {
                 cmd.env(name, value);
             }
@@ -565,12 +567,12 @@ pub fn launch(app: &CraftApp, installed: &Installed, paths: &Paths) -> Result<()
             }
             process::spawn_detached(cmd)
         }
-        _ if cfg!(target_os = "macos") && path.extension().is_some_and(|e| e == "app") => process::open(path.as_os_str()),
+        _ if cfg!(target_os = "macos") && path.extension().is_some_and(|e| e == "app") => process::open_app(path, args),
         _ => {
             if !path.exists() {
                 return Err(format!("{} isn't where it was installed ({})", app.name, path.display()));
             }
-            process::launch_binary(path, &[])
+            process::launch_binary(path, args)
         }
     }
     .map_err(|e| format!("Couldn't open {}: {e}", app.name))
diff --git a/crates/native/src/process.rs b/crates/native/src/process.rs
--- a/crates/native/src/process.rs
+++ b/crates/native/src/process.rs
@@ -296,7 +296,7 @@ fn which_on_path(name: &str) -> Option<std::path::PathBuf> {
 }
 
 /// Starts an app binary from its own folder.
-pub fn launch_binary(exe: &Path, args: &[&str]) -> Result<(), String> {
+pub fn launch_binary(exe: &Path, args: &[String]) -> Result<(), String> {
     let mut cmd = if in_sandbox() {
         let mut c = command("flatpak-spawn");
         c.arg("--host").arg(exe);
@@ -316,6 +316,15 @@ pub fn open(target: &std::ffi::OsStr) -> Result<(), String> {
     spawn_detached(opener(target)?)
 }
 
+/// macOS: opens an `.app` bundle with `open`, handing `args` to the app (`open … --args`).
+pub fn open_app(bundle: &Path, args: &[String]) -> Result<(), String> {
+    let mut cmd = opener(bundle.as_os_str())?;
+    if !args.is_empty() {
+        cmd.arg("--args").args(args);
+    }
+    spawn_detached(cmd)
+}
+
 /// The system's default opener for `target`: `explorer` on Windows, `open` on macOS, and
 /// `xdg-open` elsewhere (on the host, through `flatpak-spawn --host`, inside the sandbox).
 fn opener(target: &std::ffi::OsStr) -> Result<Command, String> {
diff --git a/crates/ui-egui/src/app.rs b/crates/ui-egui/src/app.rs
--- a/crates/ui-egui/src/app.rs
+++ b/crates/ui-egui/src/app.rs
@@ -90,7 +90,10 @@ pub(crate) struct Toast {
 pub(crate) enum UiAction {
     Command(Command),
     /// [`Command::Launch`] plus one launch-count increment.
-    Launch(String),
+    Launch {
+        app: String,
+        args: Vec<String>,
+    },
     /// Re-run the manual update check, clearing the auto-update back-off.
     CheckNow,
     /// A message a UI interaction asked to show.
@@ -108,6 +111,7 @@ pub(crate) enum Dialog {
     Replace { app: String, path: String },
     Settings,
     LauncherUpdate(artcraft_launcher_core::Release),
+    LaunchFlags { app: String, flags: String },
 }
 
 impl Dialog {
@@ -121,6 +125,7 @@ impl Dialog {
             Dialog::Replace { app, .. } => ("replace", app),
             Dialog::Settings => ("settings", ""),
             Dialog::LauncherUpdate(r) => ("launcher-update", &r.tag),
+            Dialog::LaunchFlags { app, .. } => ("launch-flags", app),
         }
     }
 }
@@ -367,10 +372,10 @@ impl LauncherUi {
             }
             match action {
                 UiAction::Command(command) => self.backend.send(command.clone()),
-                UiAction::Launch(id) => {
-                    let opened = self.settings.launches.entry(id.clone()).or_insert(0);
+                UiAction::Launch { app, args } => {
+                    let opened = self.settings.launches.entry(app.clone()).or_insert(0);
                     *opened = opened.saturating_add(1);
-                    self.backend.send(Command::Launch { app: id.clone() });
+                    self.backend.send(Command::Launch { app: app.clone(), args: args.clone() });
                 }
                 UiAction::CheckNow => {
                     self.auto_update_attempted.clear();
@@ -883,9 +888,16 @@ impl LauncherUi {
         list.into_iter().map(|(_, e)| e.app.id).collect()
     }
 
-    /// Opens an app, and counts it for "Frequently used".
+    /// Opens an app with its launch flags, and counts it for "Frequently used".
     pub(crate) fn launch_app(&mut self, id: &str) {
-        self.act(UiAction::Launch(id.to_string()));
+        let flags = self.settings.launch_flags.get(id).map_or("", String::as_str);
+        // The dialog only saves flags that split, but the settings file can be edited by hand.
+        let Ok(args) = artcraft_launcher_core::args::split(flags) else {
+            let name = self.entry(id).map_or(id, |e| e.app.name).to_string();
+            self.toast_from_ui(crate::i18n::fmt(tl!("{name} wasn't opened: a quote in its launch options isn't closed."), &[("name", &name)]), true);
+            return;
+        };
+        self.act(UiAction::Launch { app: id.to_string(), args });
     }
 
     /// Whether the app on screen under `id` is installed on this computer.
@@ -1129,9 +1141,9 @@ mod tests {
     fn a_launch_count_is_incremented_once_per_visible_frame() {
         let (mut launcher, sent, _ctx) = launcher();
         launcher.frame = Some(1);
-        launcher.act(UiAction::Launch("photocraft".into()));
+        launcher.act(UiAction::Launch { app: "photocraft".into(), args: vec![] });
         launcher.flush_actions();
-        launcher.act(UiAction::Launch("photocraft".into()));
+        launcher.act(UiAction::Launch { app: "photocraft".into(), args: vec![] });
         launcher.flush_actions();
         assert_eq!(launcher.settings.launches.get("photocraft"), Some(&1));
         let launches = sent.borrow().iter().filter(|command| matches!(command, Command::Launch { .. })).count();
diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs
--- a/crates/ui-egui/src/dialogs.rs
+++ b/crates/ui-egui/src/dialogs.rs
@@ -37,6 +37,7 @@ impl LauncherUi {
             Dialog::Replace { app, path } => modal.show(ctx, |ui| close = self.replace_dialog(ui, app, path)),
             Dialog::Settings => modal.show(ctx, |ui| close = self.settings_dialog(ui, host)),
             Dialog::LauncherUpdate(r) => modal.show(ctx, |ui| close = self.launcher_update_dialog(ui, r)),
+            Dialog::LaunchFlags { app, flags } => modal.show(ctx, |ui| close = self.launch_flags_dialog(ui, app, flags)),
         };
         if close || response.should_close() {
             self.dialog = None;
@@ -222,6 +223,41 @@ impl LauncherUi {
         close
     }
 
+    /// The extra command-line flags an app is opened with. Saved only when they split cleanly.
+    fn launch_flags_dialog(&mut self, ui: &mut egui::Ui, id: &str, flags: &mut String) -> bool {
+        let Some(app) = self.entry(id).map(|e| e.app) else { return true };
+        let t = Tokens::get(ui.ctx());
+        ui.set_width(460.0f32.min(ui.ctx().content_rect().width() - 80.0));
+        Self::title(ui, crate::icons::app(id), &fmt(tl!("{name} launch options"), &[("name", app.name)]), "");
+        let label = ui.label(tl!("Flags added each time the app is opened"));
+        ui.add(egui::TextEdit::singleline(flags).id_salt("launch_flags").hint_text("--flag \"…\"").desired_width(f32::INFINITY))
+            .labelled_by(label.id);
+        let split = artcraft_launcher_core::args::split(flags);
+        let (note, color) = match split {
+            Ok(_) => (tl!("Separate flags with spaces. Quote a value that has spaces in it."), t.muted),
+            Err(_) => (tl!("A quote isn't closed."), t.bad),
+        };
+        ui.label(RichText::new(note).color(color));
+        ui.add_space(14.0);
+        let mut close = false;
+        ui.horizontal(|ui| {
+            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
+                let save = ui.add_enabled_ui(split.is_ok(), |ui| widgets::button(ui, tl!("Save"), Kind::Primary, 90.0)).inner;
+                if save.clicked() {
+                    let trimmed = flags.trim();
+                    if trimmed.is_empty() {
+                        self.settings.launch_flags.remove(id);
+                    } else {
+                        self.settings.launch_flags.insert(id.to_string(), trimmed.to_string());
+                    }
+                    close = true;
+                }
+                close |= widgets::button(ui, tl!("Cancel"), Kind::Secondary, 90.0).clicked();
+            });
+        });
+        close
+    }
+
     /// Warns that `path` already exists and asks whether to replace it, returning `true` when the
     /// dialog should close, including when the app entry is gone. `Replace` is queued at the action
     /// boundary, not sent inline.
diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv
--- a/crates/ui-egui/src/i18n/cs.tsv
+++ b/crates/ui-egui/src/i18n/cs.tsv
@@ -59,6 +59,13 @@
 	Show in Files	Zobrazit v Souborech
 	Reinstall	Znovu nainstalovat
 	Uninstall…	Odinstalovat…
+	Launch options…	Možnosti spuštění…
+	{name} launch options	Možnosti spuštění aplikace {name}
+	Flags added each time the app is opened	Přepínače přidané při každém otevření aplikace
+	Separate flags with spaces. Quote a value that has spaces in it.	Přepínače oddělte mezerami. Hodnotu s mezerami dejte do uvozovek.
+	A quote isn't closed.	Uvozovky nejsou uzavřené.
+	{name} wasn't opened: a quote in its launch options isn't closed.	Aplikace {name} nebyla otevřena: uvozovky v jejích možnostech spuštění nejsou uzavřené.
+	Save	Uložit
 	All releases	Všechna vydání
 	ArtCraft Launcher {version} is available.	Je k dispozici ArtCraft Launcher {version}.
 	Dismiss	Skrýt
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -72,6 +72,13 @@
 	Show in Files	Im Dateimanager zeigen
 	Reinstall	Neu installieren
 	Uninstall…	Deinstallieren…
+	Launch options…	Startoptionen…
+	{name} launch options	Startoptionen für {name}
+	Flags added each time the app is opened	Flags, die bei jedem Öffnen der App angehängt werden
+	Separate flags with spaces. Quote a value that has spaces in it.	Flags durch Leerzeichen trennen. Werte mit Leerzeichen in Anführungszeichen setzen.
+	A quote isn't closed.	Ein Anführungszeichen wird nicht geschlossen.
+	{name} wasn't opened: a quote in its launch options isn't closed.	{name} wurde nicht geöffnet: Ein Anführungszeichen in den Startoptionen wird nicht geschlossen.
+	Save	Speichern
 	All releases	Alle Versionen
 	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} ist verfügbar.
 	Dismiss	Ausblenden
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -72,6 +72,13 @@
 	Show in Files	Mostrar en Archivos
 	Reinstall	Reinstalar
 	Uninstall…	Desinstalar…
+	Launch options…	Opciones de inicio…
+	{name} launch options	Opciones de inicio de {name}
+	Flags added each time the app is opened	Parámetros que se añaden cada vez que se abre la aplicación
+	Separate flags with spaces. Quote a value that has spaces in it.	Separa los parámetros con espacios. Pon entre comillas un valor que contenga espacios.
+	A quote isn't closed.	Hay unas comillas sin cerrar.
+	{name} wasn't opened: a quote in its launch options isn't closed.	{name} no se abrió: hay unas comillas sin cerrar en sus opciones de inicio.
+	Save	Guardar
 	All releases	Todas las versiones
 	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} está disponible.
 	Dismiss	Descartar
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -70,6 +70,13 @@
 	Show in Files	Afficher dans Fichiers
 	Reinstall	Réinstaller
 	Uninstall…	Désinstaller…
+	Launch options…	Options de lancement…
+	{name} launch options	Options de lancement de {name}
+	Flags added each time the app is opened	Options ajoutées à chaque ouverture de l'application
+	Separate flags with spaces. Quote a value that has spaces in it.	Séparez les options par des espaces. Mettez entre guillemets une valeur qui contient des espaces.
+	A quote isn't closed.	Un guillemet n'est pas fermé.
+	{name} wasn't opened: a quote in its launch options isn't closed.	{name} n'a pas été ouvert : un guillemet n'est pas fermé dans ses options de lancement.
+	Save	Enregistrer
 	All releases	Toutes les versions
 	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} est disponible.
 	Dismiss	Ignorer
diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv
--- a/crates/ui-egui/src/i18n/hu.tsv
+++ b/crates/ui-egui/src/i18n/hu.tsv
@@ -72,6 +72,13 @@
 	Show in Files	Megjelenítés a fájlkezelőben
 	Reinstall	Újratelepítés
 	Uninstall…	Eltávolítás…
+	Launch options…	Indítási beállítások…
+	{name} launch options	{name} indítási beállításai
+	Flags added each time the app is opened	Kapcsolók, amelyeket az alkalmazás minden megnyitásakor átad
+	Separate flags with spaces. Quote a value that has spaces in it.	A kapcsolókat szóközzel válassza el. A szóközt tartalmazó értéket tegye idézőjelbe.
+	A quote isn't closed.	Egy idézőjel nincs lezárva.
+	{name} wasn't opened: a quote in its launch options isn't closed.	A(z) {name} nem nyílt meg: az indítási beállításaiban egy idézőjel nincs lezárva.
+	Save	Mentés
 	All releases	Minden kiadás
 	ArtCraft Launcher {version} is available.	Elérhető az ArtCraft Launcher új verziója: {version}.
 	Dismiss	Elrejtés
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -74,6 +74,13 @@
 	Show in Files	ファイルマネージャーで表示
 	Reinstall	再インストール
 	Uninstall…	アンインストール…
+	Launch options…	起動オプション…
+	{name} launch options	{name} の起動オプション
+	Flags added each time the app is opened	アプリを開くたびに追加されるフラグ
+	Separate flags with spaces. Quote a value that has spaces in it.	フラグはスペースで区切ってください。スペースを含む値は引用符で囲んでください。
+	A quote isn't closed.	引用符が閉じられていません。
+	{name} wasn't opened: a quote in its launch options isn't closed.	{name} を開けませんでした: 起動オプションの引用符が閉じられていません。
+	Save	保存
 	All releases	すべてのリリース
 	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} が利用可能です。
 	Dismiss	閉じる
diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv
--- a/crates/ui-egui/src/i18n/pl.tsv
+++ b/crates/ui-egui/src/i18n/pl.tsv
@@ -73,6 +73,13 @@
 	Show in Files	Pokaż w menedżerze plików
 	Reinstall	Przeinstaluj
 	Uninstall…	Odinstaluj…
+	Launch options…	Opcje uruchamiania…
+	{name} launch options	Opcje uruchamiania aplikacji {name}
+	Flags added each time the app is opened	Flagi dodawane przy każdym otwarciu aplikacji
+	Separate flags with spaces. Quote a value that has spaces in it.	Oddziel flagi spacjami. Wartość ze spacjami umieść w cudzysłowie.
+	A quote isn't closed.	Cudzysłów nie jest zamknięty.
+	{name} wasn't opened: a quote in its launch options isn't closed.	Nie otwarto aplikacji {name}: cudzysłów w jej opcjach uruchamiania nie jest zamknięty.
+	Save	Zapisz
 	All releases	Wszystkie wydania
 	ArtCraft Launcher {version} is available.	Dostępna jest nowa wersja ArtCraft Launcher ({version}).
 	Dismiss	Zamknij
diff --git a/crates/ui-egui/src/i18n/pt-BR.tsv b/crates/ui-egui/src/i18n/pt-BR.tsv
--- a/crates/ui-egui/src/i18n/pt-BR.tsv
+++ b/crates/ui-egui/src/i18n/pt-BR.tsv
@@ -60,6 +60,13 @@
 	Show in Files	Mostrar no gerenciador de arquivos
 	Reinstall	Reinstalar
 	Uninstall…	Desinstalar…
+	Launch options…	Opções de inicialização…
+	{name} launch options	Opções de inicialização do {name}
+	Flags added each time the app is opened	Parâmetros adicionados sempre que o aplicativo é aberto
+	Separate flags with spaces. Quote a value that has spaces in it.	Separe os parâmetros com espaços. Coloque entre aspas um valor que tenha espaços.
+	A quote isn't closed.	Há aspas que não foram fechadas.
+	{name} wasn't opened: a quote in its launch options isn't closed.	{name} não foi aberto: há aspas não fechadas nas opções de inicialização.
+	Save	Salvar
 	All releases	Todos os lançamentos
 	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} está disponível.
 	Dismiss	Dispensar
diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv
--- a/crates/ui-egui/src/i18n/ru.tsv
+++ b/crates/ui-egui/src/i18n/ru.tsv
@@ -70,6 +70,13 @@
 	Show in Files	Показать в файловом менеджере
 	Reinstall	Переустановить
 	Uninstall…	Удалить…
+	Launch options…	Параметры запуска…
+	{name} launch options	Параметры запуска {name}
+	Flags added each time the app is opened	Флаги, добавляемые при каждом открытии приложения
+	Separate flags with spaces. Quote a value that has spaces in it.	Разделяйте флаги пробелами. Значение с пробелами заключите в кавычки.
+	A quote isn't closed.	Кавычка не закрыта.
+	{name} wasn't opened: a quote in its launch options isn't closed.	{name} не открыто: в параметрах запуска не закрыта кавычка.
+	Save	Сохранить
 	All releases	Все релизы
 	ArtCraft Launcher {version} is available.	Доступен ArtCraft Launcher {version}.
 	Dismiss	Закрыть
diff --git a/crates/ui-egui/src/i18n/zh-CN.tsv b/crates/ui-egui/src/i18n/zh-CN.tsv
--- a/crates/ui-egui/src/i18n/zh-CN.tsv
+++ b/crates/ui-egui/src/i18n/zh-CN.tsv
@@ -54,6 +54,13 @@
 	Release notes	更新说明
 	Reinstall	重新安装
 	Uninstall…	卸载…
+	Launch options…	启动选项…
+	{name} launch options	{name} 启动选项
+	Flags added each time the app is opened	每次打开应用时附加的参数
+	Separate flags with spaces. Quote a value that has spaces in it.	用空格分隔各个参数。含有空格的值请用引号括起来。
+	A quote isn't closed.	有引号未闭合。
+	{name} wasn't opened: a quote in its launch options isn't closed.	未打开 {name}：其启动选项中有引号未闭合。
+	Save	保存
 	All releases	所有版本
 	ArtCraft Launcher {version} is available.	ArtCraft Launcher {version} 已可更新。
 	Dismiss	忽略
diff --git a/crates/ui-egui/src/settings.rs b/crates/ui-egui/src/settings.rs
--- a/crates/ui-egui/src/settings.rs
+++ b/crates/ui-egui/src/settings.rs
@@ -122,6 +122,9 @@ pub struct Settings {
     pub tray_hint_shown: bool,
     /// How many times each app was opened from the launcher (catalog id → count), for "Frequently used".
     pub launches: BTreeMap<String, u32>,
+    /// Extra command-line flags each app is opened with (catalog id → the text as typed, split
+    /// by [`args::split`](artcraft_launcher_core::args::split) at launch).
+    pub launch_flags: BTreeMap<String, String>,
 }
 
 impl Settings {
@@ -204,6 +207,7 @@ impl Default for Settings {
             hidden: std::collections::BTreeSet::new(),
             tray_hint_shown: false,
             launches: BTreeMap::new(),
+            launch_flags: BTreeMap::new(),
         }
     }
 }
diff --git a/crates/ui-egui/src/views.rs b/crates/ui-egui/src/views.rs
--- a/crates/ui-egui/src/views.rs
+++ b/crates/ui-egui/src/views.rs
@@ -1008,13 +1008,18 @@ impl LauncherUi {
         });
     }
 
-    /// The menu items for an installed app: open, reveal in the file manager, reinstall, and
-    /// uninstall (which asks first).
+    /// The menu items for an installed app: open, its launch options, reveal in the file manager,
+    /// reinstall, and uninstall (which asks first).
     fn menu_installed(&mut self, ui: &mut Ui, status: &AppStatus, id: &str, reveal: &'static str) {
         if ui.button(tl!("Open")).clicked() {
             self.launch_app(id);
             ui.close();
         }
+        if ui.button(tl!("Launch options…")).clicked() {
+            let flags = self.settings.launch_flags.get(id).cloned().unwrap_or_default();
+            self.dialog = Some(Dialog::LaunchFlags { app: id.to_string(), flags });
+            ui.close();
+        }
         if ui.button(t(reveal)).clicked() {
             self.act_command(Command::Reveal { app: id.to_string() });
             ui.close();
diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs
--- a/crates/ui-egui/tests/ui.rs
+++ b/crates/ui-egui/tests/ui.rs
@@ -1056,6 +1056,68 @@ fn hiding_an_app_removes_it_until_it_is_unhidden() {
     assert_eq!(h.state().as_ref().unwrap().settings.filter, artcraft_launcher_ui_egui::settings::Filter::All);
 }
 
+#[test]
+fn launch_options_are_saved_per_app_and_passed_when_it_opens() {
+    use egui::accesskit::Role;
+    let (mut h, sent) = harness(ViewMode::Table, egui::Theme::Dark, true);
+    h.run_steps(6);
+    let menu = |h: &mut Harness<'static, Option<LauncherUi>>| {
+        let row = h.get_all_by_label("WordCraft").next().unwrap().rect().center().y;
+        h.get_all_by_label_contains("More actions").find(|n| (n.rect().center().y - row).abs() < 25.0).unwrap().click();
+        h.run_steps(3);
+    };
+    menu(&mut h);
+    h.get_by_label("Launch options…").click();
+    h.run_steps(3);
+    let field = "Flags added each time the app is opened";
+    h.get_by_role_and_label(Role::TextInput, field).click();
+    h.get_by_role_and_label(Role::TextInput, field).type_text(r#"--dir "My Docs" --open"#);
+    h.run_steps(3);
+    h.get_by_label("Save").click();
+    h.run_steps(3);
+    assert_eq!(h.state().as_ref().unwrap().settings.launch_flags.get("wordcraft").map(String::as_str), Some(r#"--dir "My Docs" --open"#));
+
+    menu(&mut h);
+    h.get_all_by_label("Open").last().unwrap().click(); // the menu's
+    h.run_steps(3);
+    let args: Vec<String> = ["--dir", "My Docs", "--open"].map(String::from).into();
+    assert!(sent.borrow().contains(&Command::Launch { app: "wordcraft".into(), args }));
+    // The other apps open with nothing added.
+    assert!(!h.state().as_ref().unwrap().settings.launch_flags.contains_key("lightcraft"));
+}
+
+#[test]
+fn launch_options_with_an_unclosed_quote_are_not_saved_or_launched() {
+    use egui::accesskit::Role;
+    let (mut h, sent) = harness(ViewMode::Table, egui::Theme::Dark, true);
+    h.run_steps(6);
+    let row = h.get_all_by_label("WordCraft").next().unwrap().rect().center().y;
+    h.get_all_by_label_contains("More actions").find(|n| (n.rect().center().y - row).abs() < 25.0).unwrap().click();
+    h.run_steps(3);
+    h.get_by_label("Launch options…").click();
+    h.run_steps(3);
+    let field = "Flags added each time the app is opened";
+    h.get_by_role_and_label(Role::TextInput, field).click();
+    h.get_by_role_and_label(Role::TextInput, field).type_text(r#"--dir "My Docs"#);
+    h.run_steps(3);
+    h.get_by_label("A quote isn't closed.");
+    h.get_by_label("Save").click();
+    h.run_steps(3);
+    assert!(h.state().as_ref().unwrap().settings.launch_flags.is_empty());
+    h.get_by_label("Cancel").click();
+    h.run_steps(3);
+
+    // Flags edited by hand in the settings file: the app isn't opened with half of them.
+    h.state_mut().as_mut().unwrap().settings.launch_flags.insert("wordcraft".into(), "'oops".into());
+    sent.borrow_mut().clear();
+    let row = h.get_all_by_label("WordCraft").next().unwrap().rect().center().y;
+    h.get_all_by_label_contains("More actions").find(|n| (n.rect().center().y - row).abs() < 25.0).unwrap().click();
+    h.run_steps(3);
+    h.get_all_by_label("Open").last().unwrap().click(); // the menu's
+    h.run_steps(3);
+    assert!(!sent.borrow().iter().any(|c| matches!(c, Command::Launch { .. })));
+}
+
 #[test]
 /// The view switch stores Table, and the table names each app once in its wordmark rather than
 /// again on the decorative icon.
@@ -2495,7 +2557,7 @@ fn opening_an_app_counts_and_frequently_used_puts_the_most_opened_first() {
     assert_eq!(opens.len(), 2);
     opens[1].click(); // WordCraft
     h.run_steps(3);
-    assert!(sent.borrow().contains(&Command::Launch { app: "wordcraft".into() }));
+    assert!(sent.borrow().contains(&Command::Launch { app: "wordcraft".into(), args: vec![] }));
     assert_eq!(h.state().as_ref().unwrap().settings.launches.get("wordcraft"), Some(&1));
     h.run_steps(4);
     let word = h.get_by_label("WordCraft details").rect();
