diff --git a/crates/ui-egui/src/canvas/start_screen.rs b/crates/ui-egui/src/canvas/start_screen.rs
--- a/crates/ui-egui/src/canvas/start_screen.rs
+++ b/crates/ui-egui/src/canvas/start_screen.rs
@@ -116,7 +116,7 @@ fn home_recent(app: &mut PhotocraftApp, ui: &mut egui::Ui, recent: &[String]) {
     let mut open = None;
     let mut remove = None;
     let mut clear = false;
-    for path in recent {
+    for (i, path) in recent.iter().enumerate() {
         let name = crate::file_open::display_name(path);
         let folder = std::path::Path::new(path).parent().map(|d| d.to_string_lossy().into_owned()).unwrap_or_default();
         let (row, resp) = ui.allocate_exact_size(egui::vec2(width, HOME_RECENT_ROW), Sense::click());
@@ -151,7 +151,7 @@ fn home_recent(app: &mut PhotocraftApp, ui: &mut egui::Ui, recent: &[String]) {
             }
         }
         if resp.clicked() && remove.is_none() {
-            open = Some(path.clone());
+            open = Some((i, path.clone()));
         }
         resp.context_menu(|ui| {
             if ui.button(tl!("Remove from Recent")).clicked() {
@@ -169,9 +169,16 @@ fn home_recent(app: &mut PhotocraftApp, ui: &mut egui::Ui, recent: &[String]) {
     } else if let Some(path) = remove {
         app.remove_recent(&path);
     }
-    if let Some(path) = open
-        && let Err(e) = app.open_path(&path)
-    {
-        app.open_failed(&crate::file_open::display_name(&path), &e);
+    if let Some((i, path)) = open {
+        // The row is File › Open Recent's item `i` (the list is its first entries), so synthetic
+        // input is judged as that command, as the menu does: a client confined to its automation
+        // roots can't plant a recent file outside them and click it open.
+        let gate = match app.services.automation_command.as_ref() {
+            Some(authorize) if app.automation_input => authorize(&format!("file.openRecent.{i}"), &json!({})),
+            _ => Ok(()),
+        };
+        if let Err(e) = gate.and_then(|()| app.open_path(&path)) {
+            app.open_failed(&crate::file_open::display_name(&path), &e);
+        }
     }
 }
diff --git a/crates/ui-egui/src/home_recent_gate_tests.rs b/crates/ui-egui/src/home_recent_gate_tests.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/home_recent_gate_tests.rs
@@ -0,0 +1,86 @@
+//! A Home screen recent-file row is File › Open Recent's item, so synthetic input that clicks it
+//! is judged as that command: a client confined to its automation roots can't plant a recent
+//! file outside them and click it open. A user's own click still opens it.
+
+use egui_kittest::Harness;
+use egui_kittest::kittest::Queryable;
+use serde_json::{Value, json};
+
+use crate::{PhotocraftApp, Services};
+
+/// The desktop policy's verdict on file commands: only the in-memory ones run.
+fn deny_file(id: &str, _: &Value) -> photocraft_engine::Result<()> {
+    if id.starts_with("file.") && !matches!(id, "file.new" | "file.close" | "file.closeAll" | "file.closeOthers") {
+        Err(photocraft_engine::EngineError::Other(format!("automation command `{id}` is disabled")))
+    } else {
+        Ok(())
+    }
+}
+
+/// The Home screen with `path` as its one recent file, under the desktop's automation policy.
+fn home(path: &str) -> Harness<'static, (PhotocraftApp, bool)> {
+    let services = Services {
+        automation_authorize: Some(deny_file),
+        automation_command: Some(Box::new(|id, p| deny_file(id, p).map_err(|e| e.to_string()))),
+        import: Some(Box::new(|name, bytes, opts| {
+            photocraft_io::import_with_options(name, bytes, opts).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string())
+        })),
+        ..Default::default()
+    };
+    let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), services);
+    app.background_jobs = false;
+    app.push_recent(path);
+    let mut h = Harness::builder().with_size(egui::vec2(900.0, 760.0)).build_ui_state(
+        |ui, (app, ready): &mut (PhotocraftApp, bool)| {
+            if *ready {
+                egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| crate::canvas::document_area(app, ui));
+            }
+        },
+        (app, false),
+    );
+    PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::default());
+    h.state_mut().1 = true;
+    h.run_steps(3);
+    h
+}
+
+/// Click the row, as synthetic input when `automation` is set.
+fn click(h: &mut Harness<'static, (PhotocraftApp, bool)>, automation: bool) {
+    h.get_by_label("outsideroot.png").click();
+    h.state_mut().0.automation_input = automation;
+    h.step();
+    h.state_mut().0.automation_input = false;
+    h.run_steps(2);
+}
+
+#[test]
+fn synthetic_click_on_a_home_recent_row_is_judged_as_open_recent() {
+    let dir = std::env::temp_dir().join(format!("pc-home-recent-gate-{}", std::process::id()));
+    let _ = std::fs::remove_dir_all(&dir);
+    std::fs::create_dir_all(&dir).unwrap();
+    let file = dir.join("outsideroot.png");
+    let image = photocraft_codecs::Image::from_u8(4, 4, photocraft_codecs::ChannelLayout::Rgba, [10u8, 200, 30, 255].repeat(16)).unwrap();
+    std::fs::write(&file, photocraft_codecs::encode(&image, photocraft_codecs::Format::Png, &Default::default()).unwrap()).unwrap();
+    let path = file.to_string_lossy().into_owned();
+    let paths = |h: &Harness<'static, (PhotocraftApp, bool)>| h.state().0.session.documents().iter().map(|d| d.path.clone()).collect::<Vec<_>>();
+
+    // The menu route to the same file is refused for synthetic input.
+    let mut h = home(&path);
+    let ctx = h.ctx.clone();
+    h.state_mut().0.automation_input = true;
+    assert!(crate::menus::invoke(&mut h.state_mut().0, &ctx, "file.openRecent.0", json!({})).is_err());
+    h.state_mut().0.automation_input = false;
+
+    click(&mut h, true);
+    let status = h.state().0.ui.status.clone();
+    assert!(paths(&h).is_empty(), "a synthetic click on the Home row opened {:?}", paths(&h));
+    assert!(status.contains("file.openRecent.0"), "the refusal is reported: {status:?}");
+
+    // The user's own click opens it.
+    let mut h = home(&path);
+    click(&mut h, false);
+    let status = h.state().0.ui.status.clone();
+    assert_eq!(paths(&h), [Some(path.clone())], "status: {status:?}");
+    drop(h);
+    let _ = std::fs::remove_dir_all(&dir);
+}
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -707,6 +707,8 @@ mod pen_variants_tests;
 #[cfg(test)]
 mod color_replacement_tests;
 #[cfg(test)]
+mod home_recent_gate_tests;
+#[cfg(test)]
 mod layer_shortcut_tests;
 #[cfg(test)]
 mod live_retouch_tests;
