diff --git a/crates/engine/src/cmd/scripts.rs b/crates/engine/src/cmd/scripts.rs
--- a/crates/engine/src/cmd/scripts.rs
+++ b/crates/engine/src/cmd/scripts.rs
@@ -203,6 +203,7 @@ fn install(s: &mut Session, p: &Value, panel: bool) -> Result<Value> {
     let cfg = s.config.clone().ok_or_else(|| EngineError::Other("this session has no settings folder to install scripts into".into()))?;
     let dir = if panel { PANELS_DIR } else { SCRIPTS_DIR };
     cfg.write(&format!("{dir}/{name}"), &code).map_err(|e| EngineError::Other(format!("cannot install {name}: {e}")))?;
+    s.scripts_changed();
     // After Effects asks for a restart; our menus pick it up at once.
     s.toast(if panel {
         format!("Installed ScriptUI panel {name}: find it at the bottom of the Window menu")
@@ -231,6 +232,7 @@ fn uninstall(s: &mut Session, p: &Value) -> Result<Value> {
             removed = true;
         }
     }
+    s.scripts_changed();
     if removed { Ok(json!({"removed": name})) } else { Err(bad(c, format!("`{name}` is not installed"))) }
 }
 
@@ -602,4 +604,64 @@ mod tests {
         assert!(bad_params(&s.execute("file.uninstallScript", json!({"name": "Hello.jsx"}))), "no longer installed");
         let _ = std::fs::remove_dir_all(&root);
     }
+
+    /// Edit ▸ Keyboard Shortcuts lists the scripts File ▸ Scripts lists (not ScriptUI panels),
+    /// as soon as one is installed and until it is uninstalled; a key given to one is saved with
+    /// the preset.
+    #[test]
+    fn file_scripts_entries_take_keyboard_shortcuts() {
+        use crate::shortcuts::{Keymaps, SHORTCUTS_FILE, binding_key};
+        let (mut s, root) = session_in_tmp("shortcuts");
+        let (script, panel) = (root.join("Hello.jsx"), root.join("Hello Panel.jsx"));
+        std::fs::write(&script, "hello").unwrap();
+        std::fs::write(&panel, "panel").unwrap();
+        let listed = |s: &Session, name: &str| {
+            s.shortcuts().find(&binding_key("file.runScript", &json!({"name": name}))).map(|b| (b.label.clone(), b.path.join(" ▸ "), b.defaults.clone()))
+        };
+        assert_eq!(listed(&s, "Rename Layers.jsx"), Some(("Rename Layers.jsx".into(), "File ▸ Scripts".into(), vec![])), "the bundled samples");
+        assert_eq!(listed(&s, "Hello.jsx"), None);
+        s.execute("file.installScript", json!({"path": script.to_string_lossy()})).unwrap();
+        s.execute("file.installScriptUIPanel", json!({"path": panel.to_string_lossy()})).unwrap();
+        assert_eq!(listed(&s, "Hello.jsx"), Some(("Hello.jsx".into(), "File ▸ Scripts".into(), vec![])));
+        assert_eq!(listed(&s, "Hello Panel.jsx"), None, "panels are in the Window menu");
+        let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Hello.jsx"}, "keys": "Cmd+Alt+Shift+F7"})).unwrap();
+        let key = binding_key("file.runScript", &json!({"name": "Hello.jsx"}));
+        assert_eq!((&r["command"], &r["keys"]), (&json!(key), &json!(["Cmd+Alt+Shift+F7"])), "{r}");
+        assert_eq!(s.shortcuts().shortcut_of("file.runScript", &json!({"name": "Hello.jsx"})), Some("Cmd+Alt+Shift+F7"));
+        let saved = Keymaps::from_json(&std::fs::read_to_string(root.join("settings").join(SHORTCUTS_FILE)).unwrap());
+        assert_eq!(saved.active_preset().and_then(|p| p.overrides.get(&key)), Some(&vec!["Cmd+Alt+Shift+F7".to_string()]));
+        s.execute("file.uninstallScript", json!({"name": "Hello.jsx"})).unwrap();
+        assert_eq!(listed(&s, "Hello.jsx"), None);
+        let _ = std::fs::remove_dir_all(&root);
+    }
+
+    /// `shortcuts.list` and `shortcuts.set` see a script copied into the Scripts folder by hand
+    /// after the shortcut table was built; a script the folder doesn't hold (never there, or
+    /// removed by hand) can't take a key, and Run Script File… keeps its own keys.
+    #[test]
+    fn shortcut_commands_find_scripts_copied_into_the_folder() {
+        let (mut s, root) = session_in_tmp("copied");
+        assert!(s.shortcuts().find("file.runScript").is_some(), "the table is built");
+        std::fs::create_dir_all(root.join("settings").join(SCRIPTS_DIR)).unwrap();
+        std::fs::write(root.join("settings").join(SCRIPTS_DIR).join("Copied.jsx"), "copied").unwrap();
+        let list = s.execute("shortcuts.list", json!({"query": "copied.jsx"})).unwrap();
+        assert_eq!(list["count"], 1, "{list}");
+        let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Copied.jsx"}, "keys": "Cmd+Alt+Shift+F7"})).unwrap();
+        assert_eq!(r["command"], r#"file.runScript {"name":"Copied.jsx"}"#, "{r}");
+        let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Missing.jsx"}, "keys": "Cmd+Alt+Shift+F8"}));
+        assert!(bad_params(&r), "{r:?}");
+        // The binding-key form finds a script copied in since the last lookup, too.
+        std::fs::write(root.join("settings").join(SCRIPTS_DIR).join("Second.jsx"), "second").unwrap();
+        let r = s.execute("shortcuts.set", json!({"command": r#"file.runScript {"name":"Second.jsx"}"#, "keys": "Cmd+Alt+Shift+F10"})).unwrap();
+        assert_eq!(r["command"], r#"file.runScript {"name":"Second.jsx"}"#, "{r}");
+        // Removed by hand: no longer in the folder.
+        std::fs::remove_file(root.join("settings").join(SCRIPTS_DIR).join("Copied.jsx")).unwrap();
+        std::fs::remove_file(root.join("settings").join(SCRIPTS_DIR).join("Second.jsx")).unwrap();
+        let r = s.execute("shortcuts.set", json!({"command": r#"file.runScript {"name":"Second.jsx"}"#, "keys": "Cmd+Alt+Shift+F11"}));
+        assert!(bad_params(&r), "{r:?}");
+        let r = s.execute("shortcuts.set", json!({"command": "file.runScript", "params": {"name": "Copied.jsx"}, "keys": "Cmd+Alt+Shift+F9"}));
+        assert!(bad_params(&r), "{r:?}");
+        assert_eq!(s.shortcuts().shortcut_of("file.runScript", &Value::Null), None, "Run Script File… has no key");
+        let _ = std::fs::remove_dir_all(&root);
+    }
 }
diff --git a/crates/engine/src/cmd/settings.rs b/crates/engine/src/cmd/settings.rs
--- a/crates/engine/src/cmd/settings.rs
+++ b/crates/engine/src/cmd/settings.rs
@@ -73,13 +73,21 @@ fn prefs_pages(_: &mut Session, _: &Value) -> Result<Value> {
 
 // ---------------------------------------------------------------- shortcuts
 
-/// The bindable named by `command` (+ `params`), or by a binding key (`app.settings {...}`).
-fn bindable(s: &Session, p: &Value, cmd: &str) -> Result<crate::shortcuts::Bindable> {
+/// The bindable named by `command` (+ `params`), or by a binding key (`app.settings {...}`). A
+/// File ▸ Scripts entry (`file.runScript {"name": …}`) is looked up in File ▸ Scripts' list as it
+/// is now (the Scripts folder and the bundled scripts), and a script it doesn't list is an error,
+/// not Run Script File….
+fn bindable(s: &mut Session, p: &Value, cmd: &str) -> Result<crate::shortcuts::Bindable> {
     let c = str_p(p, "command").ok_or_else(|| bad(cmd, "missing `command`"))?;
     let params = p.get("params").cloned().unwrap_or(Value::Null);
-    let t = s.shortcuts();
     let key = binding_key(c, &params);
-    t.find(&key).or_else(|| t.find(c)).cloned().ok_or_else(|| bad(cmd, format!("`{key}` can't have a shortcut (see shortcuts.list)")))
+    let script = (c == "file.runScript" && params.get("name").is_some()) || c.starts_with("file.runScript {");
+    if script {
+        s.scripts_changed();
+    }
+    let t = s.shortcuts();
+    let found = if script { t.find(&key) } else { t.find(&key).or_else(|| t.find(c)) };
+    found.cloned().ok_or_else(|| bad(cmd, format!("`{key}` can't have a shortcut (see shortcuts.list)")))
 }
 
 fn keys_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
@@ -93,6 +101,8 @@ fn keys_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
 }
 
 fn sc_list(s: &mut Session, p: &Value) -> Result<Value> {
+    // Scripts copied into the Scripts folder since the table was built are listed too.
+    s.scripts_changed();
     let q = str_p(p, "query").unwrap_or("").to_lowercase();
     let assigned = p.get("assigned").and_then(Value::as_bool).unwrap_or(false);
     let keys_q = str_p(p, "keys").and_then(normalize);
diff --git a/crates/engine/src/settings/session_settings.rs b/crates/engine/src/settings/session_settings.rs
--- a/crates/engine/src/settings/session_settings.rs
+++ b/crates/engine/src/settings/session_settings.rs
@@ -171,9 +171,19 @@ impl Session {
         self.shortcut_table = std::sync::OnceLock::new();
     }
 
-    /// The active shortcut preset resolved against every bindable command.
+    /// The scripts in the Scripts folder changed: list File ▸ Scripts' entries again in the
+    /// shortcut table.
+    pub fn scripts_changed(&mut self) {
+        self.shortcut_table = std::sync::OnceLock::new();
+    }
+
+    /// The active shortcut preset resolved against every bindable command, File ▸ Scripts'
+    /// entries included.
     pub fn shortcuts(&self) -> &ShortcutTable {
-        self.shortcut_table.get_or_init(|| ShortcutTable::build(&self.keymaps, &self.ui_commands))
+        self.shortcut_table.get_or_init(|| {
+            let scripts: Vec<String> = crate::commands::scripts::scripts(self).into_iter().filter(|e| !e.panel).map(|e| e.name).collect();
+            ShortcutTable::build_with_scripts(&self.keymaps, &self.ui_commands, &scripts)
+        })
     }
 
     /// Remember a project in File ▸ Open Recent and in the crash-recovery sentinel.
diff --git a/crates/engine/src/settings/shortcuts.rs b/crates/engine/src/settings/shortcuts.rs
--- a/crates/engine/src/settings/shortcuts.rs
+++ b/crates/engine/src/settings/shortcuts.rs
@@ -1,6 +1,7 @@
 //! Keyboard shortcut presets (Edit ▸ Keyboard Shortcuts).
 //!
-//! Every menu entry, registry command and frontend command is *bindable*: it has a binding key
+//! Every menu entry, registry command and frontend command, and every script File ▸ Scripts
+//! lists, is *bindable*: it has a binding key
 //! (the command id, plus its bound parameters for entries such as `app.settings
 //! {"page":"general"}`), a scope (application-wide or a panel) and default keys. The built-in
 //! preset "EffectCraft Default" is After Effects' default keyboard layout and is read-only; custom
@@ -142,6 +143,27 @@ pub fn bindables(ui: &[UiCommand]) -> Vec<Bindable> {
     out
 }
 
+/// The File ▸ Scripts entries of the scripts `names` (file names, as the menu lists them), with
+/// no default keys. A key is bound to the script's name, so it stays with that script when
+/// scripts are added to or removed from the folder.
+pub fn script_bindables(names: &[String]) -> Vec<Bindable> {
+    names
+        .iter()
+        .map(|n| {
+            let params = json!({"name": n});
+            Bindable {
+                key: binding_key("file.runScript", &params),
+                command: "file.runScript".into(),
+                params,
+                label: n.clone(),
+                path: vec!["File".into(), "Scripts".into()],
+                scope: APP_SCOPE.into(),
+                defaults: vec![],
+            }
+        })
+        .collect()
+}
+
 /// Canonical shortcut text (`cmd+shift+k`, `Shift+Cmd+K`, `⌘⇧K` → `Cmd+Shift+K`); `None` if it
 /// has no key.
 pub fn normalize(s: &str) -> Option<String> {
@@ -421,7 +443,16 @@ pub struct ShortcutTable {
 
 impl ShortcutTable {
     pub fn build(maps: &Keymaps, ui: &[UiCommand]) -> ShortcutTable {
-        let bindables = bindables(ui);
+        Self::build_with_scripts(maps, ui, &[])
+    }
+
+    /// [`ShortcutTable::build`] with the File ▸ Scripts entries of the scripts `scripts`, listed
+    /// after the other File ▸ Scripts entries.
+    pub fn build_with_scripts(maps: &Keymaps, ui: &[UiCommand], scripts: &[String]) -> ShortcutTable {
+        let mut bindables = bindables(ui);
+        let at = bindables.iter().rposition(|b| b.path.starts_with(&["File".to_string(), "Scripts".to_string()])).map_or(bindables.len(), |i| i + 1);
+        let new: Vec<Bindable> = script_bindables(scripts).into_iter().filter(|s| !bindables.iter().any(|b| b.key == s.key)).collect();
+        bindables.splice(at..at, new);
         let keys = bindables.iter().map(|b| (b.key.clone(), maps.keys_of(b))).collect();
         ShortcutTable { bindables, keys }
     }
diff --git a/crates/ui-egui/src/menus/bar.rs b/crates/ui-egui/src/menus/bar.rs
--- a/crates/ui-egui/src/menus/bar.rs
+++ b/crates/ui-egui/src/menus/bar.rs
@@ -162,7 +162,12 @@ fn menu_nodes(
                         "recentPresets" => app.session.prefs.recent_presets.get(i).cloned(),
                         _ => None,
                     };
-                    let r = ui.add_enabled(entry_enabled(app, e), egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str())));
+                    let mut b = egui::Button::new((gutter(entry_checked(app, e) == Some(true)), e.label.as_str()));
+                    // File ▸ Scripts entries show the key a script was given.
+                    if let Some(s) = entry_shortcut(app, e) {
+                        b = b.shortcut_text(shortcut_text(&s));
+                    }
+                    let r = ui.add_enabled(entry_enabled(app, e), b);
                     let r = match &tip {
                         Some(t) => r.on_hover_text(t),
                         None => r,
diff --git a/crates/ui-egui/src/native_menu.rs b/crates/ui-egui/src/native_menu.rs
--- a/crates/ui-egui/src/native_menu.rs
+++ b/crates/ui-egui/src/native_menu.rs
@@ -180,8 +180,9 @@ pub fn build(app: &EffectcraftApp) -> NativeMenu {
                             label: e.label.clone(),
                             command: e.command.clone(),
                             params: e.params.clone(),
-                            shortcut: None,
-                            accelerator: None,
+                            // File ▸ Scripts entries show the key a script was given.
+                            shortcut: crate::menus::entry_shortcut(app, e),
+                            accelerator: crate::menus::entry_shortcut(app, e).as_deref().and_then(accelerator),
                             enabled: crate::menus::entry_enabled(app, e),
                             checked: crate::menus::entry_checked(app, e),
                         }));
@@ -343,7 +344,12 @@ pub fn state_key(app: &EffectcraftApp) -> u64 {
     s.history.undo.iter().for_each(|(l, _)| l.hash(&mut h));
     s.state.open_comps.hash(&mut h);
     s.active_comp_id().and_then(|c| s.state.views3d.get(&c)).map(|v| format!("{:?}", v.current)).hash(&mut h);
-    s.shortcuts().keys.len().hash(&mut h);
+    // Each binding's keys: a key changed to another one gives the item a new accelerator.
+    let shortcuts = s.shortcuts();
+    for b in &shortcuts.bindables {
+        b.key.hash(&mut h);
+        shortcuts.keys.get(&b.key).hash(&mut h);
+    }
     s.keymaps.active.hash(&mut h);
     let v = &app.ui.viewer;
     (v.rulers, v.guides, v.snap_guides, v.lock_guides, v.grid, v.snap_grid, v.show_layer_controls, v.pasteboard).hash(&mut h);
@@ -705,6 +711,24 @@ mod tests {
         assert!(spec2.items().iter().any(|i| i.command == "file.openRecent" && i.label == "Recent Test.ecproj"));
     }
 
+    /// A script's key changed to another key changes the key, so the File ▸ Scripts entry gets
+    /// the new accelerator even though the number of bindings stays the same.
+    #[test]
+    fn a_changed_shortcut_changes_the_state_key() {
+        let mut app = app();
+        let script = json!({"name": "Rename Layers.jsx"});
+        let b = app.session.shortcuts().find(&effectcraft_engine::shortcuts::binding_key("file.runScript", &script)).cloned().unwrap();
+        app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F7".to_string()]).unwrap();
+        app.session.shortcuts_changed();
+        let k0 = state_key(&app);
+        app.session.keymaps.set(&b, &["Cmd+Alt+Shift+F8".to_string()]).unwrap();
+        app.session.shortcuts_changed();
+        assert_ne!(state_key(&app), k0);
+        let spec = build(&app);
+        let entry = spec.items().into_iter().find(|i| i.command == "file.runScript" && i.params == script).unwrap();
+        assert_eq!(entry.shortcut.as_deref(), Some("Cmd+Alt+Shift+F8"));
+    }
+
     /// #1630: a checked Window menu entry closes its panel; an unchecked one opens it.
     #[test]
     fn window_menu_entry_toggles_its_panel() {
diff --git a/crates/ui-egui/src/panels/shortcut_editor.rs b/crates/ui-egui/src/panels/shortcut_editor.rs
--- a/crates/ui-egui/src/panels/shortcut_editor.rs
+++ b/crates/ui-egui/src/panels/shortcut_editor.rs
@@ -49,6 +49,8 @@ const PANEL_COLOR: Color32 = Color32::from_rgb(0x3f, 0xa8, 0x6c);
 
 pub fn open(app: &mut EffectcraftApp) {
     app.dialog_state.shortcuts = EditorState::default();
+    // Scripts copied into the Scripts folder since the table was built are listed too.
+    app.session.scripts_changed();
     app.dialog = Some(Dialog::Shortcuts);
 }
 
diff --git a/crates/ui-egui/tests/it/ui_scriptui.rs b/crates/ui-egui/tests/it/ui_scriptui.rs
--- a/crates/ui-egui/tests/it/ui_scriptui.rs
+++ b/crates/ui-egui/tests/it/ui_scriptui.rs
@@ -113,6 +113,42 @@ fn panels_open_at_quit_dock_again_at_launch() {
     assert_eq!(store.read(effectcraft_engine::commands::scripts::OPEN_PANELS_FILE).as_deref(), Some(r#"{"open":[]}"#));
 }
 
+/// A script copied into the Scripts folder while the app runs is listed in Edit ▸ Keyboard
+/// Shortcuts and takes a key there: File ▸ Scripts shows the key next to the script, and
+/// pressing it runs the script as its menu entry does.
+#[test]
+fn a_key_given_to_a_script_runs_it() {
+    use effectcraft_engine::config::{ConfigStore, MemoryConfig};
+    let store = std::sync::Arc::new(MemoryConfig::default());
+    let mut s = effectcraft_host::session();
+    s.config = Some(store.clone());
+    let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
+    h.run_steps(3);
+    assert!(h.state().session.shortcuts().find(r#"file.runScript {"name":"Rename Layers.jsx"}"#).is_some(), "the shortcut table is built");
+    store.write("Scripts/Add Comp.jsx", "app.project.items.addComp('From Key', 64, 64, 1, 1, 25);").unwrap();
+    effectcraft_ui_egui::panels::shortcut_editor::open(h.state_mut());
+    let script = json!({"name": "Add Comp.jsx"});
+    let r = h.state_mut().session.execute("shortcuts.set", json!({"command": "file.runScript", "params": script, "keys": "Cmd+Alt+Shift+F7"})).unwrap();
+    assert_eq!(r["command"], r#"file.runScript {"name":"Add Comp.jsx"}"#, "{r}");
+    assert_eq!(r["keys"], json!(["Cmd+Alt+Shift+F7"]), "{r}");
+    assert_eq!(r["conflicts"], json!([]), "{r}");
+    h.state_mut().dialog = None;
+    let spec = effectcraft_ui_egui::native_menu::build(h.state());
+    let entry = spec.items().into_iter().find(|i| i.command == "file.runScript" && i.params == script).expect("the File ▸ Scripts entry");
+    assert_eq!(entry.shortcut.as_deref(), Some("Cmd+Alt+Shift+F7"));
+    let (m, k, ..) = effectcraft_ui_egui::menus::bindings(&h.state().session)
+        .into_iter()
+        .find(|b| b.2 == "file.runScript" && b.3 == script)
+        .expect("the key is bound to the script");
+    let made = |h: &Harness<'_, EffectcraftApp>| h.state().session.project.items.values().filter(|i| i.name == "From Key").count();
+    assert_eq!(made(&h), 0);
+    for pressed in [true, false] {
+        h.event(Event::Key { key: k, physical_key: Some(k), pressed, repeat: false, modifiers: m });
+        h.step();
+    }
+    assert_eq!(made(&h), 1, "the key runs the script");
+}
+
 /// The bundled Ease Presets ScriptUI panel (Window ▸ Ease Presets.jsx) docks, and clicking a
 /// preset in its list eases the selected keyframes in one undo step.
 #[test]
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -299,8 +299,9 @@ Issues #41–#47 (all from the Linux AppImage 0.1.1, 5 October): panning the vie
   on the same extension points as After Effects: scripts, ScriptUI panels and effect plug-ins
   ([plugins.md](plugins.md#extension-points-compared-with-after-effects) compares them). ScriptUI
   panels in the user's ScriptUI Panels folder are listed in the Window menu, dock like any panel
-  and open again at the next launch. Missing: the Startup and Shutdown script folders, keyboard
-  shortcuts for scripts and `.jsxbin`; `#include` / `#target` directives landed (#759,
+  and open again at the next launch. Each File ▸ Scripts entry can be given a key in Edit ▸
+  Keyboard Shortcuts. Missing: the Startup and Shutdown script folders and `.jsxbin`;
+  `#include` / `#target` directives landed (#759,
   [scripting-parity.md](scripting-parity.md)).
 - Done so far: ease presets (#254) cover what ease-curve plug-ins are used for: easing curves kept
   by name and applied to pairs of keyframes. Modelled on a third-party panel, they are not core: a
diff --git a/docs/plugins.md b/docs/plugins.md
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -17,7 +17,7 @@ Guide):
 | ExtendScript preprocessor directives (`#include`, `#target`, `#targetengine`) | `#include` / `//@include` / `#includepath` inline files relative to the script (once each, depth-limited, file gate applies); `#target`, `#targetengine`, `#strict` are ignored | Same |
 | `Scripts/Startup` and `Scripts/Shutdown`: run in alphabetical order when the app starts and quits | Not run | Gap |
 | `Scripts/ScriptUI Panels`: listed at the bottom of the Window menu, dockable like the app's own panels, `this` is the panel | `Scripts/ScriptUI Panels/` in the settings folder (File ▸ Scripts ▸ Install ScriptUI Panel… copies one there), listed at the bottom of the Window menu, docked like any panel, `this` is the panel; the panels open when the app quits open again at the next launch | Same. Where a panel was docked isn't kept: workspace layouts aren't saved between launches |
-| Keyboard shortcuts for scripts (the Keyboard Shortcuts editor lists the scripts of the Scripts folder) | Shortcuts bind to commands and menu entries; the File ▸ Scripts and Window menu entries of scripts aren't offered | Gap |
+| Keyboard shortcuts for scripts (the Keyboard Shortcuts editor lists the scripts of the Scripts folder) | Edit ▸ Keyboard Shortcuts lists the scripts of File ▸ Scripts under File ▸ Scripts (`file.runScript {"name": "<file name>"}`); a key given to one is saved with the preset, File ▸ Scripts shows it, and pressing it runs the script as its menu entry does | Same. After Effects binds a key to a script's place in the alphabetical list (`ExecuteScriptMenuItem01`…), and EffectCraft binds it to the script's file name. The Window menu entries of ScriptUI panels aren't offered |
 | Running a script from the command line (`afterfx -r script.jsx`) | `effectcraft-cli script`, the control channel's and MCP's `script.run` | Same purpose, other entry points |
 | `app.settings` (kept in the preferences between sessions) | Kept in `script_settings.json` in the settings folder (`script.settings.get` / `script.settings.save`) | Same |
 | Allow Scripts to Write Files and Access Network | Settings ▸ Scripting & Expressions | Same |
diff --git a/docs/preferences.md b/docs/preferences.md
--- a/docs/preferences.md
+++ b/docs/preferences.md
@@ -189,7 +189,8 @@ Effect Controls uses compact, fixed 20 pt parameter rows, as After Effects does:
 Edit ▸ Keyboard Shortcuts (`Cmd+Alt+'`) opens the shortcut editor: an on-screen keyboard showing
 which keys are assigned for the held modifiers (purple: application-wide, green: panel-specific,
 both: split), modifier toggles, a searchable list of every command (all engine commands, menu
-entries with bound parameters and the frontend's tool / panel commands), and a preset menu.
+entries with bound parameters, the scripts File ▸ Scripts lists and the frontend's tool / panel
+commands), and a preset menu.
 
 - **Presets.** "EffectCraft Default" is After Effects' default layout and is read-only; editing
   it creates a "Custom" copy. Custom presets store only their differences from the default and
diff --git a/docs/scripting-parity.md b/docs/scripting-parity.md
--- a/docs/scripting-parity.md
+++ b/docs/scripting-parity.md
@@ -26,7 +26,7 @@ our own scripts; no real-world script library has been run against both apps.
 | Startup / Shutdown folders | yes | not run | missing | plugins.md |
 | `#include`, `#target` directives | yes | `#include` / `//@include` / `#includepath` inline files relative to the including script (once each, nested at most 32 deep, under the scripts' file gate; errors name the included file and line); `#target`, `#targetengine`, `#strict` are ignored | good | `tests_include.rs` (#759); installed scripts and panels have no folder of their own, so they include from the project's folder or absolute paths |
 | `.jsxbin` | yes | no | out of scope | No public specification |
-| Keyboard shortcuts for scripts | yes | no | missing | plugins.md |
+| Keyboard shortcuts for scripts | yes: the scripts of the Scripts folder take keys, by their place in the alphabetical list | Edit ▸ Keyboard Shortcuts lists the scripts of File ▸ Scripts; a key stays with the script's file name | same | `file_scripts_entries_take_keyboard_shortcuts`, `a_key_given_to_a_script_runs_it` ([plugins.md](plugins.md)); ScriptUI panels in the Window menu take no key |
 | Command-line rendering `aerender` | yes | `effectcraft-cli render`, `script`, `exec` | same purpose | `apps/effectcraft-cli` |
 | Essential Graphics scripting hooks | `addToMotionGraphicsTemplate…` | yes (M13.7) | good | — |
 | Agent control (MCP, control channel, every command) | none | yes | beyond | [agents.md](agents.md), [control-protocol.md](control-protocol.md); `list_effects` omits parameter types and ranges (#441) |
