Repository: zellij-org/zellij (language: rust)

Issue:
Create a plugin permission system
As we add more and more capabilities to Zellij plugins, we should create a permission system that will give users transparency into which permission this plugin requires (eg. file system access, session management, scrollback reading and manipulation) and the ability to grant them or not.

Depends on: #317, #771


Discussion / hints:
Now that this has become relevant, I'd like to flesh it out a bit more.

## A `request_permission` API call needs to be implemented
This will be an API call similar to the calls here: https://github.com/zellij-org/zellij/blob/main/zellij-tile/src/shim.rs
As its payload, it should receive a new `PluginPermission` enum:
```rust
// pseudocode
enum PluginPermission {
    ReadScrollback,
    ReadKeyboardInput,
    ReadCursorPosition,
    // ...
}
```

Whenever a plugin calls `request_permission(Vec<PluginPermission>)`, the plugins should render a special screen instead of the plugin UI, something like:
```
Plugin <plugin_name> would like permission to:
1. Read the viewport and scrollback from all panes
2. Read keyboard input
3. Read cursor position

Would you like to grant these permissions? (y/n)
```
Once the user presses `y` or `n`, the plugins should re-render themselves, and receive a new `Event::PermissionsGranted` or `Event::PermissionsDenied` event in their `update` method.

To render this temporary UI, we could utilize the `LoadingIndication` struct (and hopefully give it a better name!): https://github.com/zellij-org/zellij/blob/main/zellij-server/src/ui/loading_indication.rs

To receive user input when the permission request screen is open, we could implement the `adjust_input_to_terminal` pane method for `PluginPane`: https://github.com/zellij-org/zellij/blob/main/zellij-server/src/tab/mod.rs#L202

## We should check for permissions for the relevant `zellij_exports` API calls
We should place a `plugin_permissions` attribute (which will be a `HashSet<PluginPermission>`) in `PluginEnv`: https://github.com/zellij-org/zellij/blob/main/zellij-server/src/plugins/plugin_map.rs#L193

Which we should fill up with the granted permissions if the user pressed `y` to grant them. We should then check against these permissions in zellij_exports whenever we call the relevant method: https://github.com/zellij-org/zellij/blob/main/zellij-server/src/plugins/zellij_exports.rs

## Caching the user response
Once the user has granted permissions, we do not want them to have to do it every time the plugin loads (or requests permissions). To do this, we should cache the response in our cache and read it every time before we request permission. If there is a response, we should use it.

The cache file should be similar to a config file, and should look something like this:
```kdl
// plugin-permissions.kdl
"file:/path/to/my/plugin.wasm" {
    ReadScrollback,
    ReadKeyboardInput,
    ReadCursorPosition,
}
"file:/path/to/my/other-plugin.wasm" {
    ReadScrollback,
}
```
It should be in the zellij *config* folder, as this is essentially a configuration file

Fix patch:
diff --git a/default-plugins/strider/src/main.rs b/default-plugins/strider/src/main.rs
index e620930c05..3d4c94a916 100644
--- a/default-plugins/strider/src/main.rs
+++ b/default-plugins/strider/src/main.rs
@@ -30,6 +30,7 @@ impl ZellijPlugin for State {
             EventType::FileSystemCreate,
             EventType::FileSystemUpdate,
             EventType::FileSystemDelete,
+            EventType::PermissionRequestResult,
         ]);
         post_message_to(PluginMessage {
             worker_name: Some("file_name_search".into()),
@@ -54,6 +55,9 @@ impl ZellijPlugin for State {
         };
         self.ev_history.push_back((event.clone(), Instant::now()));
         match event {
+            Event::PermissionRequestResult(_) => {
+                should_render = true;
+            },
             Event::Timer(_elapsed) => {
                 if self.search_state.loading {
                     set_timeout(0.5);
diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs
index 2de5b74eb2..1f2d770bb1 100644
--- a/zellij-server/src/panes/floating_panes/mod.rs
+++ b/zellij-server/src/panes/floating_panes/mod.rs
@@ -295,7 +295,7 @@ impl FloatingPanes {
             pane.render_full_viewport();
         }
     }
-    pub fn set_pane_frames(&mut self, os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
+    pub fn set_pane_frames(&mut self, _os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
         let err_context =
             |pane_id: &PaneId| format!("failed to activate frame on pane {pane_id:?}");
 
@@ -392,7 +392,7 @@ impl FloatingPanes {
         self.set_force_render();
     }
 
-    pub fn resize_pty_all_panes(&mut self, os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
+    pub fn resize_pty_all_panes(&mut self, _os_api: &mut Box<dyn ServerOsApi>) -> Result<()> {
         for pane in self.panes.values_mut() {
             resize_pty!(pane, os_api, self.senders, self.character_cell_size)
                 .with_context(|| format!("failed to resize PTY in pane {:?}", pane.pid()))?;
@@ -403,7 +403,7 @@ impl FloatingPanes {
     pub fn resize_active_pane(
         &mut self,
         client_id: ClientId,
-        os_api: &mut Box<dyn ServerOsApi>,
+        _os_api: &mut Box<dyn ServerOsApi>,
         strategy: &ResizeStrategy,
     ) -> Result<bool> {
         // true => successfully resized
@@ -838,7 +838,7 @@ impl FloatingPanes {
             self.focus_pane_for_all_clients(focused_pane);
         }
     }
-    pub fn switch_active_pane_with(&mut self, os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) {
+    pub fn switch_active_pane_with(&mut self, _os_api: &mut Box<dyn ServerOsApi>, pane_id: PaneId) {
         if let Some(active_pane_id) = self.first_active_floating_pane_id() {
             let current_position = self.panes.get(&active_pane_id).unwrap();
             let prev_geom = current_position.position_and_size();
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 4655296099..b4e3461e2d 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -1,11 +1,11 @@
-use std::collections::HashMap;
+use std::collections::{BTreeSet, HashMap};
 use std::time::Instant;
 
 use crate::output::{CharacterChunk, SixelImageChunk};
 use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId};
 use crate::plugins::PluginInstruction;
 use crate::pty::VteBytes;
-use crate::tab::Pane;
+use crate::tab::{AdjustedInput, Pane};
 use crate::ui::{
     loading_indication::LoadingIndication,
     pane_boundaries_frame::{FrameParams, PaneFrame},
@@ -13,6 +13,7 @@ use crate::ui::{
 use crate::ClientId;
 use std::cell::RefCell;
 use std::rc::Rc;
+use zellij_utils::data::{PermissionStatus, PermissionType, PluginPermission};
 use zellij_utils::pane_size::{Offset, SizeInPixels};
 use zellij_utils::position::Position;
 use zellij_utils::{
@@ -25,6 +26,15 @@ use zellij_utils::{
     vte,
 };
 
+macro_rules! style {
+    ($fg:expr) => {
+        ansi_term::Style::new().fg(match $fg {
+            PaletteColor::Rgb((r, g, b)) => ansi_term::Color::RGB(r, g, b),
+            PaletteColor::EightBit(color) => ansi_term::Color::Fixed(color),
+        })
+    };
+}
+
 macro_rules! get_or_create_grid {
     ($self:ident, $client_id:ident) => {{
         let rows = $self.get_content_rows();
@@ -73,6 +83,7 @@ pub(crate) struct PluginPane {
     pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
     invoked_with: Option<Run>,
     loading_indication: LoadingIndication,
+    requesting_permissions: Option<PluginPermission>,
     debug: bool,
 }
 
@@ -121,6 +132,7 @@ impl PluginPane {
             pane_frame_color_override: None,
             invoked_with,
             loading_indication,
+            requesting_permissions: None,
             debug,
         };
         for client_id in currently_connected_clients {
@@ -181,6 +193,14 @@ impl Pane for PluginPane {
     }
     fn handle_plugin_bytes(&mut self, client_id: ClientId, bytes: VteBytes) {
         self.set_client_should_render(client_id, true);
+
+        let mut vte_bytes = bytes;
+        if let Some(plugin_permission) = &self.requesting_permissions {
+            vte_bytes = self
+                .display_request_permission_message(plugin_permission)
+                .into();
+        }
+
         let grid = get_or_create_grid!(self, client_id);
 
         // this is part of the plugin contract, whenever we update the plugin and call its render function, we delete the existing viewport
@@ -193,14 +213,36 @@ impl Pane for PluginPane {
             .vte_parsers
             .entry(client_id)
             .or_insert_with(|| vte::Parser::new());
-        for &byte in &bytes {
+
+        for &byte in &vte_bytes {
             vte_parser.advance(grid, byte);
         }
+
         self.should_render.insert(client_id, true);
     }
     fn cursor_coordinates(&self) -> Option<(usize, usize)> {
         None
     }
+    fn adjust_input_to_terminal(&mut self, input_bytes: Vec<u8>) -> Option<AdjustedInput> {
+        if let Some(requesting_permissions) = &self.requesting_permissions {
+            let permissions = requesting_permissions.permissions.clone();
+            match input_bytes.as_slice() {
+                // Y or y
+                &[89] | &[121] => Some(AdjustedInput::PermissionRequestResult(
+                    permissions,
+                    PermissionStatus::Granted,
+                )),
+                // N or n
+                &[78] | &[110] => Some(AdjustedInput::PermissionRequestResult(
+                    permissions,
+                    PermissionStatus::Denied,
+                )),
+                _ => None,
+            }
+        } else {
+            Some(AdjustedInput::WriteBytesToTerminal(input_bytes))
+        }
+    }
     fn position_and_size(&self) -> PaneGeom {
         self.geom
     }
@@ -233,6 +275,9 @@ impl Pane for PluginPane {
     fn set_selectable(&mut self, selectable: bool) {
         self.selectable = selectable;
     }
+    fn request_permissions_from_user(&mut self, permissions: Option<PluginPermission>) {
+        self.requesting_permissions = permissions;
+    }
     fn render(
         &mut self,
         client_id: Option<ClientId>,
@@ -595,4 +640,54 @@ impl PluginPane {
             self.handle_plugin_bytes(client_id, bytes.clone());
         }
     }
+    fn display_request_permission_message(&self, plugin_permission: &PluginPermission) -> String {
+        let bold_white = style!(self.style.colors.white).bold();
+        let cyan = style!(self.style.colors.cyan).bold();
+        let orange = style!(self.style.colors.orange).bold();
+        let green = style!(self.style.colors.green).bold();
+
+        let mut messages = String::new();
+        let permissions: BTreeSet<PermissionType> =
+            plugin_permission.permissions.clone().into_iter().collect();
+
+        let min_row_count = permissions.len() + 4;
+
+        if self.rows() >= min_row_count {
+            messages.push_str(&format!(
+                "{} {} {}\n",
+                bold_white.paint("Plugin"),
+                cyan.paint(&plugin_permission.name),
+                bold_white.paint("asks permission to:"),
+            ));
+            permissions.iter().enumerate().for_each(|(i, p)| {
+                messages.push_str(&format!(
+                    "\n\r{}. {}",
+                    bold_white.paint(&format!("{}", i + 1)),
+                    orange.paint(p.display_name())
+                ));
+            });
+
+            messages.push_str(&format!(
+                "\n\n\r{} {}",
+                bold_white.paint("Allow?"),
+                green.paint("(y/n)"),
+            ));
+        } else {
+            messages.push_str(&format!(
+                "{} {}. {} {}\n",
+                bold_white.paint("This plugin asks permission to:"),
+                orange.paint(
+                    permissions
+                        .iter()
+                        .map(|p| p.to_string())
+                        .collect::<Vec<_>>()
+                        .join(", ")
+                ),
+                bold_white.paint("Allow?"),
+                green.paint("(y/n)"),
+            ));
+        }
+
+        messages
+    }
 }
diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs
index 9aca1af4ae..736c16bc95 100644
--- a/zellij-server/src/panes/tiled_panes/mod.rs
+++ b/zellij-server/src/panes/tiled_panes/mod.rs
@@ -68,7 +68,6 @@ pub struct TiledPanes {
     draw_pane_frames: bool,
     panes_to_hide: HashSet<PaneId>,
     fullscreen_is_active: bool,
-    os_api: Box<dyn ServerOsApi>,
     senders: ThreadSenders,
     window_title: Option<String>,
     client_id_to_boundaries: HashMap<ClientId, Boundaries>,
@@ -105,7 +104,6 @@ impl TiledPanes {
             draw_pane_frames,
             panes_to_hide: HashSet::new(),
             fullscreen_is_active: false,
-            os_api,
             senders,
             window_title: None,
             client_id_to_boundaries: HashMap::new(),
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index f36bdc2d2e..4bef0683d1 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -18,7 +18,7 @@ use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
 use wasm_bridge::WasmBridge;
 
 use zellij_utils::{
-    data::{Event, EventType, PluginCapabilities},
+    data::{Event, EventType, PermissionStatus, PermissionType, PluginCapabilities},
     errors::{prelude::*, ContextType, PluginContext},
     input::{
         command::TerminalAction,
@@ -79,6 +79,13 @@ pub enum PluginInstruction {
         String, // serialized payload
     ),
     PluginSubscribedToEvents(PluginId, ClientId, HashSet<EventType>),
+    PermissionRequestResult(
+        PluginId,
+        Option<ClientId>,
+        Vec<PermissionType>,
+        PermissionStatus,
+        Option<PathBuf>,
+    ),
     Exit,
 }
 
@@ -105,6 +112,9 @@ impl From<&PluginInstruction> for PluginContext {
             PluginInstruction::PluginSubscribedToEvents(..) => {
                 PluginContext::PluginSubscribedToEvents
             },
+            PluginInstruction::PermissionRequestResult(..) => {
+                PluginContext::PermissionRequestResult
+            },
         }
     }
 }
@@ -287,6 +297,30 @@ pub(crate) fn plugin_thread_main(
                     }
                 }
             },
+            PluginInstruction::PermissionRequestResult(
+                plugin_id,
+                client_id,
+                permissions,
+                status,
+                cache_path,
+            ) => {
+                if let Err(e) = wasm_bridge.cache_plugin_permissions(
+                    plugin_id,
+                    client_id,
+                    permissions,
+                    status,
+                    cache_path,
+                ) {
+                    log::error!("{}", e);
+                }
+
+                let updates = vec![(
+                    Some(plugin_id),
+                    client_id,
+                    Event::PermissionRequestResult(status),
+                )];
+                wasm_bridge.update_plugins(updates)?;
+            },
             PluginInstruction::Exit => {
                 wasm_bridge.cleanup();
                 break;
diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs
index 92a4c04807..ea0c2b6a68 100644
--- a/zellij-server/src/plugins/plugin_loader.rs
+++ b/zellij-server/src/plugins/plugin_loader.rs
@@ -188,6 +188,7 @@ impl<'a> PluginLoader<'a> {
         display_loading_stage!(end, loading_indication, senders, plugin_id);
         Ok(())
     }
+
     pub fn add_client(
         client_id: ClientId,
         plugin_dir: PathBuf,
@@ -613,6 +614,19 @@ impl<'a> PluginLoader<'a> {
         }
         start_function.call(&[]).with_context(err_context)?;
 
+        plugin_map.lock().unwrap().insert(
+            self.plugin_id,
+            self.client_id,
+            Arc::new(Mutex::new(RunningPlugin::new(
+                main_user_instance,
+                main_user_env,
+                self.size.rows,
+                self.size.cols,
+            ))),
+            subscriptions.clone(),
+            workers,
+        );
+
         let protobuf_plugin_configuration: ProtobufPluginConfiguration = self
             .plugin
             .userspace_configuration
@@ -640,18 +654,6 @@ impl<'a> PluginLoader<'a> {
             self.senders,
             self.plugin_id
         );
-        plugin_map.lock().unwrap().insert(
-            self.plugin_id,
-            self.client_id,
-            Arc::new(Mutex::new(RunningPlugin::new(
-                main_user_instance,
-                main_user_env,
-                self.size.rows,
-                self.size.cols,
-            ))),
-            subscriptions.clone(),
-            workers,
-        );
         display_loading_stage!(
             indicate_writing_plugin_to_cache_success,
             self.loading_indication,
@@ -764,13 +766,13 @@ impl<'a> PluginLoader<'a> {
             })
             .with_context(err_context)?;
         let wasi = wasi_env.import_object(&module).with_context(err_context)?;
-
         let mut mut_plugin = self.plugin.clone();
         mut_plugin.set_tab_index(self.tab_index);
         let plugin_env = PluginEnv {
             plugin_id: self.plugin_id,
             client_id: self.client_id,
             plugin: mut_plugin,
+            permissions: Arc::new(Mutex::new(None)),
             senders: self.senders.clone(),
             wasi_env,
             plugin_own_data_dir: self.plugin_own_data_dir.clone(),
diff --git a/zellij-server/src/plugins/plugin_map.rs b/zellij-server/src/plugins/plugin_map.rs
index 2f7454c7ec..0cf26943e5 100644
--- a/zellij-server/src/plugins/plugin_map.rs
+++ b/zellij-server/src/plugins/plugin_map.rs
@@ -11,7 +11,6 @@ use wasmer_wasi::WasiEnv;
 use crate::{thread_bus::ThreadSenders, ClientId};
 
 use zellij_utils::async_channel::Sender;
-use zellij_utils::errors::prelude::*;
 use zellij_utils::{
     data::EventType,
     data::PluginCapabilities,
@@ -20,6 +19,7 @@ use zellij_utils::{
     input::plugins::PluginConfig,
     ipc::ClientAttributes,
 };
+use zellij_utils::{data::PermissionType, errors::prelude::*};
 
 // the idea here is to provide atomicity when adding/removing plugins from the map (eg. when a new
 // client connects) but to also allow updates/renders not to block each other
@@ -193,6 +193,7 @@ pub type Subscriptions = HashSet<EventType>;
 pub struct PluginEnv {
     pub plugin_id: PluginId,
     pub plugin: PluginConfig,
+    pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>,
     pub senders: ThreadSenders,
     pub wasi_env: WasiEnv,
     pub tab_index: usize,
@@ -215,6 +216,10 @@ impl PluginEnv {
             self.plugin_id
         )
     }
+
+    pub fn set_permissions(&mut self, permissions: HashSet<PermissionType>) {
+        self.permissions.lock().unwrap().replace(permissions);
+    }
 }
 
 #[derive(Eq, PartialEq, Hash)]
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index d6fcf1fc6e..5c3c7c0d2c 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -13,6 +13,8 @@ use std::{
 };
 use wasmer::{Instance, Module, Store, Value};
 use zellij_utils::async_std::task::{self, JoinHandle};
+use zellij_utils::data::{PermissionStatus, PermissionType};
+use zellij_utils::input::permission::PermissionCache;
 use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
 use zellij_utils::plugin_api::event::ProtobufEvent;
 
@@ -706,6 +708,44 @@ impl WasmBridge {
             };
         }
     }
+    pub fn cache_plugin_permissions(
+        &mut self,
+        plugin_id: PluginId,
+        client_id: Option<ClientId>,
+        permissions: Vec<PermissionType>,
+        status: PermissionStatus,
+        cache_path: Option<PathBuf>,
+    ) -> Result<()> {
+        if let Some(running_plugin) = self
+            .plugin_map
+            .lock()
+            .unwrap()
+            .get_running_plugin(plugin_id, client_id)
+        {
+            let err_context = || format!("Failed to write plugin permission {plugin_id}");
+
+            let mut running_plugin = running_plugin.lock().unwrap();
+            let permissions = if status == PermissionStatus::Granted {
+                permissions
+            } else {
+                vec![]
+            };
+
+            running_plugin
+                .plugin_env
+                .set_permissions(HashSet::from_iter(permissions.clone()));
+
+            let mut permission_cache = PermissionCache::from_path_or_default(cache_path);
+            permission_cache.cache(
+                running_plugin.plugin_env.plugin.location.to_string(),
+                permissions,
+            );
+
+            permission_cache.write_to_file().with_context(err_context)?;
+        }
+
+        Ok(())
+    }
 }
 
 fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) {
@@ -728,6 +768,35 @@ fn handle_plugin_loading_failure(
     ));
 }
 
+// TODO: move to permissions?
+fn check_event_permission(
+    plugin_env: &PluginEnv,
+    event: &Event,
+) -> (PermissionStatus, Option<PermissionType>) {
+    if plugin_env.plugin.is_builtin() {
+        // built-in plugins can do all the things because they're part of the application and
+        // there's no use to deny them anything
+        return (PermissionStatus::Granted, None);
+    }
+    let permission = match event {
+        Event::ModeUpdate(..)
+        | Event::TabUpdate(..)
+        | Event::PaneUpdate(..)
+        | Event::CopyToClipboard(..)
+        | Event::SystemClipboardFailure
+        | Event::InputReceived => PermissionType::ReadApplicationState,
+        _ => return (PermissionStatus::Granted, None),
+    };
+
+    if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
+        if permissions.contains(&permission) {
+            return (PermissionStatus::Granted, None);
+        }
+    }
+
+    (PermissionStatus::Denied, Some(permission))
+}
+
 pub fn apply_event_to_plugin(
     plugin_id: PluginId,
     client_id: ClientId,
@@ -739,35 +808,48 @@ pub fn apply_event_to_plugin(
     plugin_bytes: &mut Vec<(PluginId, ClientId, Vec<u8>)>,
 ) -> Result<()> {
     let err_context = || format!("Failed to apply event to plugin {plugin_id}");
-    let protobuf_event: ProtobufEvent = event
-        .clone()
-        .try_into()
-        .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
-    let update = instance
-        .exports
-        .get_function("update")
-        .with_context(err_context)?;
-    wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec())
-        .with_context(err_context)?;
-    let update_return = update.call(&[]).with_context(err_context)?;
-    let should_render = match update_return.get(0) {
-        Some(Value::I32(n)) => *n == 1,
-        _ => false,
-    };
-
-    if rows > 0 && columns > 0 && should_render {
-        let rendered_bytes = instance
-            .exports
-            .get_function("render")
-            .map_err(anyError::new)
-            .and_then(|render| {
-                render
-                    .call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
+    match check_event_permission(plugin_env, event) {
+        (PermissionStatus::Granted, _) => {
+            let protobuf_event: ProtobufEvent = event
+                .clone()
+                .try_into()
+                .map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
+            let update = instance
+                .exports
+                .get_function("update")
+                .with_context(err_context)?;
+            wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec())
+                .with_context(err_context)?;
+            let update_return = update.call(&[]).with_context(err_context)?;
+            let should_render = match update_return.get(0) {
+                Some(Value::I32(n)) => *n == 1,
+                _ => false,
+            };
+            if rows > 0 && columns > 0 && should_render {
+                let rendered_bytes = instance
+                    .exports
+                    .get_function("render")
                     .map_err(anyError::new)
-            })
-            .and_then(|_| wasi_read_string(&plugin_env.wasi_env))
-            .with_context(err_context)?;
-        plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec()));
+                    .and_then(|render| {
+                        render
+                            .call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
+                            .map_err(anyError::new)
+                    })
+                    .and_then(|_| wasi_read_string(&plugin_env.wasi_env))
+                    .with_context(err_context)?;
+                plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec()));
+            }
+        },
+        (PermissionStatus::Denied, permission) => {
+            log::error!(
+                "PluginId '{}' permission '{}' is not allowed - Event '{:?}' denied",
+                plugin_id,
+                permission
+                    .map(|p| p.to_string())
+                    .unwrap_or("UNKNOWN".to_owned()),
+                EventType::from_str(&event.to_string()).with_context(err_context)?
+            );
+        },
     }
     Ok(())
 }
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 275de13e6d..f2effa19a0 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -8,12 +8,15 @@ use std::{
     collections::{BTreeMap, HashSet},
     path::PathBuf,
     process,
+    str::FromStr,
     sync::{Arc, Mutex},
     thread,
     time::{Duration, Instant},
 };
 use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
 use wasmer_wasi::WasiEnv;
+use zellij_utils::data::{CommandType, PermissionStatus, PermissionType, PluginPermission};
+use zellij_utils::input::permission::PermissionCache;
 
 use url::Url;
 
@@ -86,118 +89,134 @@ impl ForeignFunctionEnv {
 }
 
 fn host_run_plugin_command(env: &ForeignFunctionEnv) {
+    let err_context = || format!("failed to run plugin command {}", env.plugin_env.name());
     wasi_read_bytes(&env.plugin_env.wasi_env)
         .and_then(|bytes| {
             let command: ProtobufPluginCommand = ProtobufPluginCommand::decode(bytes.as_slice())?;
             let command: PluginCommand = command
                 .try_into()
                 .map_err(|e| anyhow!("failed to convert serialized command: {}", e))?;
-            match command {
-                PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?,
-                PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?,
-                PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable),
-                PluginCommand::GetPluginIds => get_plugin_ids(env),
-                PluginCommand::GetZellijVersion => get_zellij_version(env),
-                PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open),
-                PluginCommand::OpenFileFloating(file_to_open) => {
-                    open_file_floating(env, file_to_open)
+            match check_command_permission(&env.plugin_env, &command) {
+                (PermissionStatus::Granted, _) => match command {
+                    PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?,
+                    PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?,
+                    PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable),
+                    PluginCommand::GetPluginIds => get_plugin_ids(env),
+                    PluginCommand::GetZellijVersion => get_zellij_version(env),
+                    PluginCommand::OpenFile(file_to_open) => open_file(env, file_to_open),
+                    PluginCommand::OpenFileFloating(file_to_open) => {
+                        open_file_floating(env, file_to_open)
+                    },
+                    PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?),
+                    PluginCommand::OpenTerminalFloating(cwd) => {
+                        open_terminal_floating(env, cwd.path.try_into()?)
+                    },
+                    PluginCommand::OpenCommandPane(command_to_run) => {
+                        open_command_pane(env, command_to_run)
+                    },
+                    PluginCommand::OpenCommandPaneFloating(command_to_run) => {
+                        open_command_pane_floating(env, command_to_run)
+                    },
+                    PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
+                    PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
+                    PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line),
+                    PluginCommand::PostMessageTo(plugin_message) => {
+                        post_message_to(env, plugin_message)?
+                    },
+                    PluginCommand::PostMessageToPlugin(plugin_message) => {
+                        post_message_to_plugin(env, plugin_message)?
+                    },
+                    PluginCommand::HideSelf => hide_self(env)?,
+                    PluginCommand::ShowSelf(should_float_if_hidden) => {
+                        show_self(env, should_float_if_hidden)
+                    },
+                    PluginCommand::SwitchToMode(input_mode) => {
+                        switch_to_mode(env, input_mode.try_into()?)
+                    },
+                    PluginCommand::NewTabsWithLayout(raw_layout) => {
+                        new_tabs_with_layout(env, &raw_layout)?
+                    },
+                    PluginCommand::NewTab => new_tab(env),
+                    PluginCommand::GoToNextTab => go_to_next_tab(env),
+                    PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
+                    PluginCommand::Resize(resize_payload) => resize(env, resize_payload),
+                    PluginCommand::ResizeWithDirection(resize_strategy) => {
+                        resize_with_direction(env, resize_strategy)
+                    },
+                    PluginCommand::FocusNextPane => focus_next_pane(env),
+                    PluginCommand::FocusPreviousPane => focus_previous_pane(env),
+                    PluginCommand::MoveFocus(direction) => move_focus(env, direction),
+                    PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction),
+                    PluginCommand::Detach => detach(env),
+                    PluginCommand::EditScrollback => edit_scrollback(env),
+                    PluginCommand::Write(bytes) => write(env, bytes),
+                    PluginCommand::WriteChars(chars) => write_chars(env, chars),
+                    PluginCommand::ToggleTab => toggle_tab(env),
+                    PluginCommand::MovePane => move_pane(env),
+                    PluginCommand::MovePaneWithDirection(direction) => {
+                        move_pane_with_direction(env, direction)
+                    },
+                    PluginCommand::ClearScreen => clear_screen(env),
+                    PluginCommand::ScrollUp => scroll_up(env),
+                    PluginCommand::ScrollDown => scroll_down(env),
+                    PluginCommand::ScrollToTop => scroll_to_top(env),
+                    PluginCommand::ScrollToBottom => scroll_to_bottom(env),
+                    PluginCommand::PageScrollUp => page_scroll_up(env),
+                    PluginCommand::PageScrollDown => page_scroll_down(env),
+                    PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
+                    PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
+                    PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env),
+                    PluginCommand::UndoRenamePane => undo_rename_pane(env),
+                    PluginCommand::CloseFocus => close_focus(env),
+                    PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env),
+                    PluginCommand::CloseFocusedTab => close_focused_tab(env),
+                    PluginCommand::UndoRenameTab => undo_rename_tab(env),
+                    PluginCommand::QuitZellij => quit_zellij(env),
+                    PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
+                    PluginCommand::NextSwapLayout => next_swap_layout(env),
+                    PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
+                    PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
+                    PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
+                    PluginCommand::StartOrReloadPlugin(plugin_url) => {
+                        start_or_reload_plugin(env, &plugin_url)?
+                    },
+                    PluginCommand::CloseTerminalPane(terminal_pane_id) => {
+                        close_terminal_pane(env, terminal_pane_id)
+                    },
+                    PluginCommand::ClosePluginPane(plugin_pane_id) => {
+                        close_plugin_pane(env, plugin_pane_id)
+                    },
+                    PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => {
+                        focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden)
+                    },
+                    PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => {
+                        focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden)
+                    },
+                    PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => {
+                        rename_terminal_pane(env, terminal_pane_id, &new_name)
+                    },
+                    PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => {
+                        rename_plugin_pane(env, plugin_pane_id, &new_name)
+                    },
+                    PluginCommand::RenameTab(tab_index, new_name) => {
+                        rename_tab(env, tab_index, &new_name)
+                    },
+                    PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload),
+                    PluginCommand::RequestPluginPermissions(permissions) => {
+                        request_permission(env, permissions)?
+                    },
                 },
-                PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?),
-                PluginCommand::OpenTerminalFloating(cwd) => {
-                    open_terminal_floating(env, cwd.path.try_into()?)
+                (PermissionStatus::Denied, permission) => {
+                    log::error!(
+                        "Plugin '{}' permission '{}' denied - Command '{:?}' denied",
+                        env.plugin_env.name(),
+                        permission
+                            .map(|p| p.to_string())
+                            .unwrap_or("UNKNOWN".to_owned()),
+                        CommandType::from_str(&command.to_string()).with_context(err_context)?
+                    );
                 },
-                PluginCommand::OpenCommandPane(command_to_run) => {
-                    open_command_pane(env, command_to_run)
-                },
-                PluginCommand::OpenCommandPaneFloating(command_to_run) => {
-                    open_command_pane_floating(env, command_to_run)
-                },
-                PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
-                PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
-                PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line),
-                PluginCommand::PostMessageTo(plugin_message) => {
-                    post_message_to(env, plugin_message)?
-                },
-                PluginCommand::PostMessageToPlugin(plugin_message) => {
-                    post_message_to_plugin(env, plugin_message)?
-                },
-                PluginCommand::HideSelf => hide_self(env)?,
-                PluginCommand::ShowSelf(should_float_if_hidden) => {
-                    show_self(env, should_float_if_hidden)
-                },
-                PluginCommand::SwitchToMode(input_mode) => {
-                    switch_to_mode(env, input_mode.try_into()?)
-                },
-                PluginCommand::NewTabsWithLayout(raw_layout) => {
-                    new_tabs_with_layout(env, &raw_layout)?
-                },
-                PluginCommand::NewTab => new_tab(env),
-                PluginCommand::GoToNextTab => go_to_next_tab(env),
-                PluginCommand::GoToPreviousTab => go_to_previous_tab(env),
-                PluginCommand::Resize(resize_payload) => resize(env, resize_payload),
-                PluginCommand::ResizeWithDirection(resize_strategy) => {
-                    resize_with_direction(env, resize_strategy)
-                },
-                PluginCommand::FocusNextPane => focus_next_pane(env),
-                PluginCommand::FocusPreviousPane => focus_previous_pane(env),
-                PluginCommand::MoveFocus(direction) => move_focus(env, direction),
-                PluginCommand::MoveFocusOrTab(direction) => move_focus_or_tab(env, direction),
-                PluginCommand::Detach => detach(env),
-                PluginCommand::EditScrollback => edit_scrollback(env),
-                PluginCommand::Write(bytes) => write(env, bytes),
-                PluginCommand::WriteChars(chars) => write_chars(env, chars),
-                PluginCommand::ToggleTab => toggle_tab(env),
-                PluginCommand::MovePane => move_pane(env),
-                PluginCommand::MovePaneWithDirection(direction) => {
-                    move_pane_with_direction(env, direction)
-                },
-                PluginCommand::ClearScreen => clear_screen(env),
-                PluginCommand::ScrollUp => scroll_up(env),
-                PluginCommand::ScrollDown => scroll_down(env),
-                PluginCommand::ScrollToTop => scroll_to_top(env),
-                PluginCommand::ScrollToBottom => scroll_to_bottom(env),
-                PluginCommand::PageScrollUp => page_scroll_up(env),
-                PluginCommand::PageScrollDown => page_scroll_down(env),
-                PluginCommand::ToggleFocusFullscreen => toggle_focus_fullscreen(env),
-                PluginCommand::TogglePaneFrames => toggle_pane_frames(env),
-                PluginCommand::TogglePaneEmbedOrEject => toggle_pane_embed_or_eject(env),
-                PluginCommand::UndoRenamePane => undo_rename_pane(env),
-                PluginCommand::CloseFocus => close_focus(env),
-                PluginCommand::ToggleActiveTabSync => toggle_active_tab_sync(env),
-                PluginCommand::CloseFocusedTab => close_focused_tab(env),
-                PluginCommand::UndoRenameTab => undo_rename_tab(env),
-                PluginCommand::QuitZellij => quit_zellij(env),
-                PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
-                PluginCommand::NextSwapLayout => next_swap_layout(env),
-                PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
-                PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
-                PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
-                PluginCommand::StartOrReloadPlugin(plugin_url) => {
-                    start_or_reload_plugin(env, &plugin_url)?
-                },
-                PluginCommand::CloseTerminalPane(terminal_pane_id) => {
-                    close_terminal_pane(env, terminal_pane_id)
-                },
-                PluginCommand::ClosePluginPane(plugin_pane_id) => {
-                    close_plugin_pane(env, plugin_pane_id)
-                },
-                PluginCommand::FocusTerminalPane(terminal_pane_id, should_float_if_hidden) => {
-                    focus_terminal_pane(env, terminal_pane_id, should_float_if_hidden)
-                },
-                PluginCommand::FocusPluginPane(plugin_pane_id, should_float_if_hidden) => {
-                    focus_plugin_pane(env, plugin_pane_id, should_float_if_hidden)
-                },
-                PluginCommand::RenameTerminalPane(terminal_pane_id, new_name) => {
-                    rename_terminal_pane(env, terminal_pane_id, &new_name)
-                },
-                PluginCommand::RenamePluginPane(plugin_pane_id, new_name) => {
-                    rename_plugin_pane(env, plugin_pane_id, &new_name)
-                },
-                PluginCommand::RenameTab(tab_index, new_name) => {
-                    rename_tab(env, tab_index, &new_name)
-                },
-                PluginCommand::ReportPanic(crash_payload) => report_panic(env, &crash_payload),
-            }
+            };
             Ok(())
         })
         .with_context(|| format!("failed to run plugin command {}", env.plugin_env.name()))
@@ -255,6 +274,30 @@ fn set_selectable(env: &ForeignFunctionEnv, selectable: bool) {
     }
 }
 
+fn request_permission(env: &ForeignFunctionEnv, permissions: Vec<PermissionType>) -> Result<()> {
+    if PermissionCache::from_path_or_default(None)
+        .check_permissions(env.plugin_env.plugin.location.to_string(), &permissions)
+    {
+        return env
+            .plugin_env
+            .senders
+            .send_to_plugin(PluginInstruction::PermissionRequestResult(
+                env.plugin_env.plugin_id,
+                Some(env.plugin_env.client_id),
+                permissions.to_vec(),
+                PermissionStatus::Granted,
+                None,
+            ));
+    }
+
+    env.plugin_env
+        .senders
+        .send_to_screen(ScreenInstruction::RequestPluginPermissions(
+            env.plugin_env.plugin_id,
+            PluginPermission::new(env.plugin_env.plugin.location.to_string(), permissions),
+        ))
+}
+
 fn get_plugin_ids(env: &ForeignFunctionEnv) {
     let ids = PluginIds {
         plugin_id: env.plugin_env.plugin_id,
@@ -1001,3 +1044,81 @@ pub fn wasi_read_bytes(wasi_env: &WasiEnv) -> Result<Vec<u8>> {
         .and_then(|string| serde_json::from_str(&string).map_err(anyError::new))
         .with_context(|| format!("failed to deserialize object from WASI env '{wasi_env:?}'"))
 }
+
+// TODO: move to permissions?
+fn check_command_permission(
+    plugin_env: &PluginEnv,
+    command: &PluginCommand,
+) -> (PermissionStatus, Option<PermissionType>) {
+    if plugin_env.plugin.is_builtin() {
+        // built-in plugins can do all the things because they're part of the application and
+        // there's no use to deny them anything
+        return (PermissionStatus::Granted, None);
+    }
+    let permission = match command {
+        PluginCommand::OpenFile(..) | PluginCommand::OpenFileFloating(..) => {
+            PermissionType::OpenFiles
+        },
+        PluginCommand::OpenTerminal(..)
+        | PluginCommand::StartOrReloadPlugin(..)
+        | PluginCommand::OpenTerminalFloating(..) => PermissionType::OpenTerminalsOrPlugins,
+        PluginCommand::OpenCommandPane(..)
+        | PluginCommand::OpenCommandPaneFloating(..)
+        | PluginCommand::ExecCmd(..) => PermissionType::RunCommands,
+        PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin,
+        PluginCommand::SwitchTabTo(..)
+        | PluginCommand::SwitchToMode(..)
+        | PluginCommand::NewTabsWithLayout(..)
+        | PluginCommand::NewTab
+        | PluginCommand::GoToNextTab
+        | PluginCommand::GoToPreviousTab
+        | PluginCommand::Resize(..)
+        | PluginCommand::ResizeWithDirection(..)
+        | PluginCommand::FocusNextPane
+        | PluginCommand::MoveFocus(..)
+        | PluginCommand::MoveFocusOrTab(..)
+        | PluginCommand::Detach
+        | PluginCommand::EditScrollback
+        | PluginCommand::ToggleTab
+        | PluginCommand::MovePane
+        | PluginCommand::MovePaneWithDirection(..)
+        | PluginCommand::ClearScreen
+        | PluginCommand::ScrollUp
+        | PluginCommand::ScrollDown
+        | PluginCommand::ScrollToTop
+        | PluginCommand::ScrollToBottom
+        | PluginCommand::PageScrollUp
+        | PluginCommand::PageScrollDown
+        | PluginCommand::ToggleFocusFullscreen
+        | PluginCommand::TogglePaneFrames
+        | PluginCommand::TogglePaneEmbedOrEject
+        | PluginCommand::UndoRenamePane
+        | PluginCommand::CloseFocus
+        | PluginCommand::ToggleActiveTabSync
+        | PluginCommand::CloseFocusedTab
+        | PluginCommand::UndoRenameTab
+        | PluginCommand::QuitZellij
+        | PluginCommand::PreviousSwapLayout
+        | PluginCommand::NextSwapLayout
+        | PluginCommand::GoToTabName(..)
+        | PluginCommand::FocusOrCreateTab(..)
+        | PluginCommand::GoToTab(..)
+        | PluginCommand::CloseTerminalPane(..)
+        | PluginCommand::ClosePluginPane(..)
+        | PluginCommand::FocusTerminalPane(..)
+        | PluginCommand::FocusPluginPane(..)
+        | PluginCommand::RenameTerminalPane(..)
+        | PluginCommand::RenamePluginPane(..)
+        | PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
+        _ => return (PermissionStatus::Granted, None),
+    };
+
+    log::info!("plugin permissions: {:?}", plugin_env.permissions);
+    if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
+        if permissions.contains(&permission) {
+            return (PermissionStatus::Granted, None);
+        }
+    }
+
+    (PermissionStatus::Denied, Some(permission))
+}
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 4d5775f7da..456bea0985 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -6,7 +6,7 @@ use std::path::PathBuf;
 use std::rc::Rc;
 use std::str;
 
-use zellij_utils::data::{Direction, PaneManifest, Resize, ResizeStrategy};
+use zellij_utils::data::{Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy};
 use zellij_utils::errors::prelude::*;
 use zellij_utils::input::command::RunCommand;
 use zellij_utils::input::options::Clipboard;
@@ -280,6 +280,10 @@ pub enum ScreenInstruction {
     FocusPaneWithId(PaneId, bool, ClientId),        // bool is should_float
     RenamePane(PaneId, Vec<u8>),
     RenameTab(usize, Vec<u8>),
+    RequestPluginPermissions(
+        u32, // u32 - plugin_id
+        PluginPermission,
+    ),
     BreakPane(Box<Layout>, Option<TerminalAction>, ClientId),
     BreakPaneRight(ClientId),
     BreakPaneLeft(ClientId),
@@ -450,6 +454,9 @@ impl From<&ScreenInstruction> for ScreenContext {
             ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId,
             ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane,
             ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab,
+            ScreenInstruction::RequestPluginPermissions(..) => {
+                ScreenContext::RequestPluginPermissions
+            },
             ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane,
             ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight,
             ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft,
@@ -2958,6 +2965,24 @@ pub(crate) fn screen_thread_main(
                 }
                 screen.report_tab_state()?;
             },
+            ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
+                let all_tabs = screen.get_tabs_mut();
+                let found = all_tabs.values_mut().any(|tab| {
+                    if tab.has_plugin(plugin_id) {
+                        tab.request_plugin_permissions(plugin_id, Some(plugin_permission.clone()));
+                        true
+                    } else {
+                        false
+                    }
+                });
+
+                if !found {
+                    log::error!(
+                        "PluginId '{}' not found - cannot request permissions",
+                        plugin_id
+                    );
+                }
+            },
             ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => {
                 screen.break_pane(default_shell, default_layout, client_id)?;
             },
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 20fbaaca29..fb6d4e170f 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -9,7 +9,9 @@ mod swap_layouts;
 use copy_command::CopyCommand;
 use std::env::temp_dir;
 use uuid::Uuid;
-use zellij_utils::data::{Direction, PaneInfo, ResizeStrategy};
+use zellij_utils::data::{
+    Direction, PaneInfo, PermissionStatus, PermissionType, PluginPermission, ResizeStrategy,
+};
 use zellij_utils::errors::prelude::*;
 use zellij_utils::input::command::RunCommand;
 use zellij_utils::position::{Column, Line};
@@ -220,6 +222,7 @@ pub trait Pane {
     fn set_should_render_boundaries(&mut self, _should_render: bool) {}
     fn selectable(&self) -> bool;
     fn set_selectable(&mut self, selectable: bool);
+    fn request_permissions_from_user(&mut self, _permissions: Option<PluginPermission>) {}
     fn render(
         &mut self,
         client_id: Option<ClientId>,
@@ -473,6 +476,7 @@ pub trait Pane {
 pub enum AdjustedInput {
     WriteBytesToTerminal(Vec<u8>),
     ReRunCommandInThisPane(RunCommand),
+    PermissionRequestResult(Vec<PermissionType>, PermissionStatus),
     CloseThisPane,
 }
 pub fn get_next_terminal_position(
@@ -1560,17 +1564,35 @@ impl Tab {
                         self.close_pane(PaneId::Terminal(active_terminal_id), false, None);
                         should_update_ui = true;
                     },
+                    Some(_) => {},
                     None => {},
                 }
             },
-            PaneId::Plugin(pid) => {
-                let mut plugin_updates = vec![];
-                for key in parse_keys(&input_bytes) {
-                    plugin_updates.push((Some(pid), client_id, Event::Key(key)));
-                }
-                self.senders
-                    .send_to_plugin(PluginInstruction::Update(plugin_updates))
-                    .with_context(err_context)?;
+            PaneId::Plugin(pid) => match active_terminal.adjust_input_to_terminal(input_bytes) {
+                Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => {
+                    let mut plugin_updates = vec![];
+                    for key in parse_keys(&adjusted_input) {
+                        plugin_updates.push((Some(pid), client_id, Event::Key(key)));
+                    }
+                    self.senders
+                        .send_to_plugin(PluginInstruction::Update(plugin_updates))
+                        .with_context(err_context)?;
+                },
+                Some(AdjustedInput::PermissionRequestResult(permissions, status)) => {
+                    self.request_plugin_permissions(pid, None);
+                    self.senders
+                        .send_to_plugin(PluginInstruction::PermissionRequestResult(
+                            pid,
+                            client_id,
+                            permissions,
+                            status,
+                            None,
+                        ))
+                        .with_context(err_context)?;
+                    should_update_ui = true;
+                },
+                Some(_) => {},
+                None => {},
             },
         }
         Ok(should_update_ui)
@@ -3437,6 +3459,20 @@ impl Tab {
         }
         Ok(())
     }
+    pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option<PluginPermission>) {
+        if let Some(plugin_pane) = self
+            .tiled_panes
+            .get_pane_mut(PaneId::Plugin(pid))
+            .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid)))
+            .or_else(|| {
+                self.suppressed_panes
+                    .values_mut()
+                    .find(|s_p| s_p.pid() == PaneId::Plugin(pid))
+            })
+        {
+            plugin_pane.request_permissions_from_user(permissions);
+        }
+    }
 }
 
 pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box<dyn Pane>) -> PaneInfo {
diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs
index 67b4a551cd..df99e9ae73 100644
--- a/zellij-tile/src/lib.rs
+++ b/zellij-tile/src/lib.rs
@@ -107,6 +107,7 @@ macro_rules! register_plugin {
         fn load() {
             STATE.with(|state| {
                 use std::collections::BTreeMap;
+                use std::convert::TryFrom;
                 use std::convert::TryInto;
                 use zellij_tile::shim::plugin_api::action::ProtobufPluginConfiguration;
                 use zellij_tile::shim::prost::Message;
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index c3a772f96d..6204767adc 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -39,6 +39,13 @@ pub fn set_selectable(selectable: bool) {
     unsafe { host_run_plugin_command() };
 }
 
+pub fn request_permission(permissions: &[PermissionType]) {
+    let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into());
+    let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+    object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+    unsafe { host_run_plugin_command() };
+}
+
 // Query Functions
 /// Returns the unique Zellij pane ID for the plugin as well as the Zellij process id.
 pub fn get_plugin_ids() -> PluginIds {
diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm
index f38c500fcb..9e2112cf9c 100755
Binary files a/zellij-utils/assets/plugins/compact-bar.wasm and b/zellij-utils/assets/plugins/compact-bar.wasm differ
diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm
index 58beaacc8f..e18d4edf9c 100755
Binary files a/zellij-utils/assets/plugins/status-bar.wasm and b/zellij-utils/assets/plugins/status-bar.wasm differ
diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm
index db61209a86..3aff9cc30e 100755
Binary files a/zellij-utils/assets/plugins/strider.wasm and b/zellij-utils/assets/plugins/strider.wasm differ
diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm
index 1a2919f6f4..ee53ba180a 100755
Binary files a/zellij-utils/assets/plugins/tab-bar.wasm and b/zellij-utils/assets/plugins/tab-bar.wasm differ
diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs
index 50600a9342..a2c3a42a50 100644
--- a/zellij-utils/src/consts.rs
+++ b/zellij-utils/src/consts.rs
@@ -36,6 +36,8 @@ lazy_static! {
         .cache_dir()
         .to_path_buf()
         .join(format!("{}", Uuid::new_v4()));
+    pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf =
+        ZELLIJ_CACHE_DIR.join("permissions.kdl");
 }
 
 pub const FEATURES: &[&str] = &[
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index 9c1c8e2f60..a08e642f91 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -6,7 +6,7 @@ use std::collections::{HashMap, HashSet};
 use std::fmt;
 use std::path::{Path, PathBuf};
 use std::str::FromStr;
-use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString};
+use strum_macros::{Display, EnumDiscriminants, EnumIter, EnumString, ToString};
 
 pub type ClientId = u16; // TODO: merge with crate type?
 
@@ -493,6 +493,63 @@ pub enum Event {
     FileSystemUpdate(Vec<PathBuf>),
     /// A file was deleted somewhere in the Zellij CWD folder
     FileSystemDelete(Vec<PathBuf>),
+    /// A Result of plugin permission request
+    PermissionRequestResult(PermissionStatus),
+}
+
+#[derive(
+    Debug,
+    PartialEq,
+    Eq,
+    Hash,
+    Copy,
+    Clone,
+    EnumDiscriminants,
+    ToString,
+    Serialize,
+    Deserialize,
+    PartialOrd,
+    Ord,
+)]
+#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize, Display, PartialOrd, Ord))]
+#[strum_discriminants(name(PermissionType))]
+#[non_exhaustive]
+pub enum Permission {
+    ReadApplicationState,
+    ChangeApplicationState,
+    OpenFiles,
+    RunCommands,
+    OpenTerminalsOrPlugins,
+    WriteToStdin,
+}
+
+impl PermissionType {
+    pub fn display_name(&self) -> String {
+        match self {
+            PermissionType::ReadApplicationState => {
+                "Access Zellij state (Panes, Tabs and UI)".to_owned()
+            },
+            PermissionType::ChangeApplicationState => {
+                "Change Zellij state (Panes, Tabs and UI)".to_owned()
+            },
+            PermissionType::OpenFiles => "Open files (eg. for editing)".to_owned(),
+            PermissionType::RunCommands => "Run commands".to_owned(),
+            PermissionType::OpenTerminalsOrPlugins => "Start new terminals and plugins".to_owned(),
+            PermissionType::WriteToStdin => "Write to standard input (STDIN)".to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct PluginPermission {
+    pub name: String,
+    pub permissions: Vec<PermissionType>,
+}
+
+impl PluginPermission {
+    pub fn new(name: String, permissions: Vec<PermissionType>) -> Self {
+        PluginPermission { name, permissions }
+    }
 }
 
 /// Describes the different input modes, which change the way that keystrokes will be interpreted.
@@ -811,6 +868,12 @@ pub enum CopyDestination {
     System,
 }
 
+#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
+pub enum PermissionStatus {
+    Granted,
+    Denied,
+}
+
 #[derive(Debug, Default, Clone)]
 pub struct FileToOpen {
     pub path: PathBuf,
@@ -882,7 +945,9 @@ impl PluginMessage {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, EnumDiscriminants, ToString)]
+#[strum_discriminants(derive(EnumString, Hash, Serialize, Deserialize))]
+#[strum_discriminants(name(CommandType))]
 pub enum PluginCommand {
     Subscribe(HashSet<EventType>),
     Unsubscribe(HashSet<EventType>),
@@ -950,4 +1015,5 @@ pub enum PluginCommand {
     RenamePluginPane(u32, String),   // plugin pane id, new name
     RenameTab(u32, String),          // tab index, new name
     ReportPanic(String),             // stringified panic
+    RequestPluginPermissions(Vec<PermissionType>),
 }
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index df5d02782c..306db448c2 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -338,6 +338,7 @@ pub enum ScreenContext {
     FocusPaneWithId,
     RenamePane,
     RenameTab,
+    RequestPluginPermissions,
     BreakPane,
     BreakPaneRight,
     BreakPaneLeft,
@@ -377,6 +378,7 @@ pub enum PluginContext {
     PostMessageToPluginWorker,
     PostMessageToPlugin,
     PluginSubscribedToEvents,
+    PermissionRequestResult,
 }
 
 /// Stack call representations corresponding to the different types of [`ClientInstruction`]s.
diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs
index 6722cfd886..1c95f348b1 100644
--- a/zellij-utils/src/input/mod.rs
+++ b/zellij-utils/src/input/mod.rs
@@ -4,6 +4,7 @@ pub mod config;
 pub mod keybinds;
 pub mod layout;
 pub mod options;
+pub mod permission;
 pub mod plugins;
 pub mod theme;
 
diff --git a/zellij-utils/src/input/permission.rs b/zellij-utils/src/input/permission.rs
new file mode 100644
index 0000000000..1755fe1e66
--- /dev/null
+++ b/zellij-utils/src/input/permission.rs
@@ -0,0 +1,60 @@
+use std::{
+    collections::HashMap,
+    fs::{self, File},
+    io::Write,
+    path::PathBuf,
+};
+
+use crate::{consts::ZELLIJ_PLUGIN_PERMISSIONS_CACHE, data::PermissionType};
+
+pub type GrantedPermission = HashMap<String, Vec<PermissionType>>;
+
+#[derive(Default, Debug)]
+pub struct PermissionCache {
+    path: PathBuf,
+    granted: GrantedPermission,
+}
+
+impl PermissionCache {
+    pub fn cache(&mut self, plugin_name: String, permissions: Vec<PermissionType>) {
+        self.granted.insert(plugin_name, permissions);
+    }
+
+    pub fn get_permissions(&self, plugin_name: String) -> Option<&Vec<PermissionType>> {
+        self.granted.get(&plugin_name)
+    }
+
+    pub fn check_permissions(
+        &self,
+        plugin_name: String,
+        permissions: &Vec<PermissionType>,
+    ) -> bool {
+        if let Some(target) = self.granted.get(&plugin_name) {
+            if target == permissions {
+                return true;
+            }
+        }
+
+        false
+    }
+
+    pub fn from_path_or_default(cache_path: Option<PathBuf>) -> Self {
+        let cache_path = cache_path.unwrap_or(ZELLIJ_PLUGIN_PERMISSIONS_CACHE.to_path_buf());
+
+        let granted = match fs::read_to_string(cache_path.clone()) {
+            Ok(raw_string) => PermissionCache::from_string(raw_string).unwrap_or_default(),
+            Err(_) => GrantedPermission::default(),
+        };
+
+        PermissionCache {
+            path: cache_path,
+            granted,
+        }
+    }
+
+    pub fn write_to_file(&self) -> std::io::Result<()> {
+        let mut f = File::create(&self.path)?;
+        write!(f, "{}", PermissionCache::to_string(&self.granted))?;
+        Ok(())
+    }
+}
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index a75a4a638e..13a03ad15d 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -1,15 +1,16 @@
 mod kdl_layout_parser;
-use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, Resize};
+use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, PermissionType, Resize};
 use crate::envs::EnvironmentVariables;
 use crate::input::config::{Config, ConfigError, KdlError};
 use crate::input::keybinds::Keybinds;
 use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation};
 use crate::input::options::{Clipboard, OnForceClose, Options};
+use crate::input::permission::{GrantedPermission, PermissionCache};
 use crate::input::plugins::{PluginConfig, PluginTag, PluginType, PluginsConfig};
 use crate::input::theme::{FrameConfig, Theme, Themes, UiConfig};
 use crate::setup::{find_default_config_dir, get_layout_dir};
 use kdl_layout_parser::KdlLayoutParser;
-use std::collections::{BTreeMap, HashMap};
+use std::collections::{BTreeMap, HashMap, HashSet};
 use strum::IntoEnumIterator;
 
 use miette::NamedSource;
@@ -1793,6 +1794,53 @@ impl Themes {
     }
 }
 
+impl PermissionCache {
+    pub fn from_string(raw_string: String) -> Result<GrantedPermission, ConfigError> {
+        let kdl_document: KdlDocument = raw_string.parse()?;
+
+        let mut granted_permission = GrantedPermission::default();
+
+        for node in kdl_document.nodes() {
+            if let Some(children) = node.children() {
+                let key = kdl_name!(node);
+                let permissions: Vec<PermissionType> = children
+                    .nodes()
+                    .iter()
+                    .filter_map(|p| {
+                        let v = kdl_name!(p);
+                        PermissionType::from_str(v).ok()
+                    })
+                    .collect();
+
+                granted_permission.insert(key.into(), permissions);
+            }
+        }
+
+        Ok(granted_permission)
+    }
+
+    pub fn to_string(granted: &GrantedPermission) -> String {
+        let mut kdl_doucment = KdlDocument::new();
+
+        granted.iter().for_each(|(k, v)| {
+            let mut node = KdlNode::new(k.as_str());
+            let mut children = KdlDocument::new();
+
+            let permissions: HashSet<PermissionType> = v.clone().into_iter().collect();
+            permissions.iter().for_each(|f| {
+                let n = KdlNode::new(f.to_string().as_str());
+                children.nodes_mut().push(n);
+            });
+
+            node.set_children(children);
+            kdl_doucment.nodes_mut().push(node);
+        });
+
+        kdl_doucment.fmt();
+        kdl_doucment.to_string()
+    }
+}
+
 pub fn parse_plugin_user_configuration(
     plugin_block: &KdlNode,
 ) -> Result<BTreeMap<String, String>, ConfigError> {
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index 95928ae3b3..3a9838fa80 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -38,6 +38,7 @@ enum EventType {
     FileSystemUpdate = 13;
     /// A file was deleted somewhere in the Zellij CWD folder
     FileSystemDelete = 14;
+    PermissionRequestResult = 15;
 }
 
 message EventNameList {
@@ -57,9 +58,14 @@ message Event {
     bool visible_payload = 9;
     CustomMessagePayload custom_message_payload = 10;
     FileListPayload file_list_payload = 11;
+    PermissionRequestResultPayload permission_request_result_payload = 12;
   }
 }
 
+message PermissionRequestResultPayload {
+  bool granted = 1;
+}
+
 message FileListPayload {
   repeated string paths = 1;
 }
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index 315fa54b81..6115fe1e82 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -13,9 +13,10 @@ pub use super::generated_api::api::{
     style::Style as ProtobufStyle,
 };
 use crate::data::{
-    CopyDestination, Direction, Event, EventType, InputMode, Key, ModeInfo, Mouse, Palette,
-    PaletteColor, PaneInfo, PaneManifest, PluginCapabilities, Style, TabInfo, ThemeHue,
+    CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
+    PermissionStatus, PluginCapabilities, Style, TabInfo,
 };
+
 use crate::errors::prelude::*;
 use crate::input::actions::Action;
 
@@ -160,6 +161,16 @@ impl TryFrom<ProtobufEvent> for Event {
                 },
                 _ => Err("Malformed payload for the file system delete Event"),
             },
+            Some(ProtobufEventType::PermissionRequestResult) => match protobuf_event.payload {
+                Some(ProtobufEventPayload::PermissionRequestResultPayload(payload)) => {
+                    if payload.granted {
+                        Ok(Event::PermissionRequestResult(PermissionStatus::Granted))
+                    } else {
+                        Ok(Event::PermissionRequestResult(PermissionStatus::Denied))
+                    }
+                },
+                _ => Err("Malformed payload for the file system delete Event"),
+            },
             None => Err("Unknown Protobuf Event"),
         }
     }
@@ -290,6 +301,18 @@ impl TryFrom<Event> for ProtobufEvent {
                     payload: Some(event::Payload::FileListPayload(file_list_payload)),
                 })
             },
+            Event::PermissionRequestResult(permission_status) => {
+                let granted = match permission_status {
+                    PermissionStatus::Granted => true,
+                    PermissionStatus::Denied => false,
+                };
+                Ok(ProtobufEvent {
+                    name: ProtobufEventType::PermissionRequestResult as i32,
+                    payload: Some(event::Payload::PermissionRequestResultPayload(
+                        PermissionRequestResultPayload { granted },
+                    )),
+                })
+            },
         }
     }
 }
@@ -673,6 +696,7 @@ impl TryFrom<ProtobufEventType> for EventType {
             ProtobufEventType::FileSystemRead => EventType::FileSystemRead,
             ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate,
             ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete,
+            ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult,
         })
     }
 }
@@ -696,6 +720,7 @@ impl TryFrom<EventType> for ProtobufEventType {
             EventType::FileSystemRead => ProtobufEventType::FileSystemRead,
             EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate,
             EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete,
+            EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult,
         })
     }
 }
@@ -717,6 +742,7 @@ fn serialize_mode_update_event() {
 
 #[test]
 fn serialize_mode_update_event_with_non_default_values() {
+    use crate::data::{Direction, Palette, PaletteColor, ThemeHue};
     use prost::Message;
     let mode_update_event = Event::ModeUpdate(ModeInfo {
         mode: InputMode::Locked,
diff --git a/zellij-utils/src/plugin_api/mod.rs b/zellij-utils/src/plugin_api/mod.rs
index 55ace5007b..4812d468c0 100644
--- a/zellij-utils/src/plugin_api/mod.rs
+++ b/zellij-utils/src/plugin_api/mod.rs
@@ -7,6 +7,7 @@ pub mod key;
 pub mod message;
 pub mod plugin_command;
 pub mod plugin_ids;
+pub mod plugin_permission;
 pub mod resize;
 pub mod style;
 pub mod generated_api {
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index f09ccf4b14..9ba8bb7f48 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -6,6 +6,7 @@ import "command.proto";
 import "message.proto";
 import "input_mode.proto";
 import "resize.proto";
+import "plugin_permission.proto";
 
 package api.plugin_command;
 
@@ -76,6 +77,7 @@ enum CommandName {
   RenamePluginPane = 63;
   RenameTab = 64;
   ReportCrash = 65;
+  RequestPluginPermissions = 66;
 }
 
 message PluginCommand {
@@ -117,9 +119,14 @@ message PluginCommand {
     IdAndNewName rename_plugin_pane_payload = 35;
     IdAndNewName rename_tab_payload = 36;
     string report_crash_payload = 37;
+    RequestPluginPermissionPayload request_plugin_permission_payload = 38;
   }
 }
 
+message RequestPluginPermissionPayload {
+  repeated plugin_permission.PermissionType permissions = 1;
+}
+
 message SubscribePayload {
   event.EventNameList subscriptions = 1;
 }
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index bf359dd202..ff50a6a767 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -3,14 +3,15 @@ pub use super::generated_api::api::{
     plugin_command::{
         plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload,
         OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat,
-        PluginCommand as ProtobufPluginCommand, PluginMessagePayload, ResizePayload,
-        SetTimeoutPayload, SubscribePayload, SwitchTabToPayload, SwitchToModePayload,
-        UnsubscribePayload,
+        PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
+        RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload,
+        SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload,
     },
+    plugin_permission::PermissionType as ProtobufPermissionType,
     resize::ResizeAction as ProtobufResizeAction,
 };
 
-use crate::data::PluginCommand;
+use crate::data::{PermissionType, PluginCommand};
 
 use std::convert::TryFrom;
 
@@ -486,6 +487,19 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
                 },
                 _ => Err("Mismatched payload for ReportCrash"),
             },
+            Some(CommandName::RequestPluginPermissions) => match protobuf_plugin_command.payload {
+                Some(Payload::RequestPluginPermissionPayload(payload)) => {
+                    Ok(PluginCommand::RequestPluginPermissions(
+                        payload
+                            .permissions
+                            .iter()
+                            .filter_map(|p| ProtobufPermissionType::from_i32(*p))
+                            .filter_map(|p| PermissionType::try_from(p).ok())
+                            .collect(),
+                    ))
+                },
+                _ => Err("Mismatched payload for RequestPluginPermission"),
+            },
             None => Err("Unrecognized plugin command"),
         }
     }
@@ -820,6 +834,18 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
                 name: CommandName::ReportCrash as i32,
                 payload: Some(Payload::ReportCrashPayload(payload)),
             }),
+            PluginCommand::RequestPluginPermissions(permissions) => Ok(ProtobufPluginCommand {
+                name: CommandName::RequestPluginPermissions as i32,
+                payload: Some(Payload::RequestPluginPermissionPayload(
+                    RequestPluginPermissionPayload {
+                        permissions: permissions
+                            .iter()
+                            .filter_map(|p| ProtobufPermissionType::try_from(*p).ok())
+                            .map(|p| p as i32)
+                            .collect(),
+                    },
+                )),
+            }),
         }
     }
 }
diff --git a/zellij-utils/src/plugin_api/plugin_permission.proto b/zellij-utils/src/plugin_api/plugin_permission.proto
new file mode 100644
index 0000000000..50072b1bbd
--- /dev/null
+++ b/zellij-utils/src/plugin_api/plugin_permission.proto
@@ -0,0 +1,12 @@
+syntax = "proto3";
+
+package api.plugin_permission;
+
+enum PermissionType {
+  ReadApplicationState = 0;
+  ChangeApplicationState = 1;
+  OpenFiles = 2;
+  RunCommands = 3;
+  OpenTerminalsOrPlugins = 4;
+  WriteToStdin = 5;
+}
diff --git a/zellij-utils/src/plugin_api/plugin_permission.rs b/zellij-utils/src/plugin_api/plugin_permission.rs
new file mode 100644
index 0000000000..d2839face4
--- /dev/null
+++ b/zellij-utils/src/plugin_api/plugin_permission.rs
@@ -0,0 +1,44 @@
+pub use super::generated_api::api::plugin_permission::PermissionType as ProtobufPermissionType;
+use crate::data::PermissionType;
+
+use std::convert::TryFrom;
+
+impl TryFrom<ProtobufPermissionType> for PermissionType {
+    type Error = &'static str;
+    fn try_from(protobuf_permission: ProtobufPermissionType) -> Result<Self, &'static str> {
+        match protobuf_permission {
+            ProtobufPermissionType::ReadApplicationState => {
+                Ok(PermissionType::ReadApplicationState)
+            },
+            ProtobufPermissionType::ChangeApplicationState => {
+                Ok(PermissionType::ChangeApplicationState)
+            },
+            ProtobufPermissionType::OpenFiles => Ok(PermissionType::OpenFiles),
+            ProtobufPermissionType::RunCommands => Ok(PermissionType::RunCommands),
+            ProtobufPermissionType::OpenTerminalsOrPlugins => {
+                Ok(PermissionType::OpenTerminalsOrPlugins)
+            },
+            ProtobufPermissionType::WriteToStdin => Ok(PermissionType::WriteToStdin),
+        }
+    }
+}
+
+impl TryFrom<PermissionType> for ProtobufPermissionType {
+    type Error = &'static str;
+    fn try_from(permission: PermissionType) -> Result<Self, &'static str> {
+        match permission {
+            PermissionType::ReadApplicationState => {
+                Ok(ProtobufPermissionType::ReadApplicationState)
+            },
+            PermissionType::ChangeApplicationState => {
+                Ok(ProtobufPermissionType::ChangeApplicationState)
+            },
+            PermissionType::OpenFiles => Ok(ProtobufPermissionType::OpenFiles),
+            PermissionType::RunCommands => Ok(ProtobufPermissionType::RunCommands),
+            PermissionType::OpenTerminalsOrPlugins => {
+                Ok(ProtobufPermissionType::OpenTerminalsOrPlugins)
+            },
+            PermissionType::WriteToStdin => Ok(ProtobufPermissionType::WriteToStdin),
+        }
+    }
+}


Test patch:
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index fe8b034b90..811a4507ea 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -39,6 +39,16 @@ register_worker!(TestWorker, test_worker, TEST_WORKER);
 
 impl ZellijPlugin for State {
     fn load(&mut self, configuration: BTreeMap<String, String>) {
+        request_permission(&[
+            PermissionType::ChangeApplicationState,
+            PermissionType::ReadApplicationState,
+            PermissionType::ReadApplicationState,
+            PermissionType::ChangeApplicationState,
+            PermissionType::OpenFiles,
+            PermissionType::RunCommands,
+            PermissionType::OpenTerminalsOrPlugins,
+            PermissionType::WriteToStdin,
+        ]);
         self.configuration = configuration;
         subscribe(&[
             EventType::InputReceived,
@@ -227,6 +237,9 @@ impl ZellijPlugin for State {
                 Key::Ctrl('z') => {
                     go_to_tab_name(&format!("{:?}", self.configuration));
                 },
+                Key::Ctrl('1') => {
+                    request_permission(&[PermissionType::ReadApplicationState]);
+                },
                 _ => {},
             },
             Event::CustomMessage(message, payload) => {
diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs
index 663ca94eec..e995a07b92 100644
--- a/zellij-server/src/plugins/unit/plugin_tests.rs
+++ b/zellij-server/src/plugins/unit/plugin_tests.rs
@@ -6,9 +6,10 @@ use std::collections::BTreeMap;
 use std::path::PathBuf;
 use tempfile::tempdir;
 use wasmer::Store;
-use zellij_utils::data::{Event, Key, PluginCapabilities};
+use zellij_utils::data::{Event, Key, PermissionStatus, PermissionType, PluginCapabilities};
 use zellij_utils::errors::ErrorContext;
 use zellij_utils::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation};
+use zellij_utils::input::permission::PermissionCache;
 use zellij_utils::input::plugins::PluginsConfig;
 use zellij_utils::ipc::ClientAttributes;
 use zellij_utils::lazy_static::lazy_static;
@@ -52,6 +53,157 @@ macro_rules! log_actions_in_thread {
     };
 }
 
+macro_rules! grant_permissions_and_log_actions_in_thread {
+    ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => {
+        std::thread::Builder::new()
+            .name("fake_screen_thread".to_string())
+            .spawn({
+                let log = $arc_mutex_log.clone();
+                let mut exit_event_count = 0;
+                let cache_path = $cache_path.clone();
+                let plugin_thread_sender = $plugin_thread_sender.clone();
+                move || loop {
+                    let (event, _err_ctx) = $receiver
+                        .recv()
+                        .expect("failed to receive event on channel");
+                    match event {
+                        $exit_event(..) => {
+                            exit_event_count += 1;
+                            log.lock().unwrap().push(event);
+                            if exit_event_count == $exit_after_count {
+                                break;
+                            }
+                        },
+                        ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => {
+                            if plugin_permission.permissions.contains($permission_type) {
+                                let _ = plugin_thread_sender.send(
+                                    PluginInstruction::PermissionRequestResult(
+                                        0,
+                                        Some($client_id),
+                                        plugin_permission.permissions,
+                                        PermissionStatus::Granted,
+                                        Some(cache_path.clone()),
+                                    ),
+                                );
+                            } else {
+                                let _ = plugin_thread_sender.send(
+                                    PluginInstruction::PermissionRequestResult(
+                                        0,
+                                        Some($client_id),
+                                        plugin_permission.permissions,
+                                        PermissionStatus::Denied,
+                                        Some(cache_path.clone()),
+                                    ),
+                                );
+                            }
+                        },
+                        _ => {
+                            log.lock().unwrap().push(event);
+                        },
+                    }
+                }
+            })
+            .unwrap()
+    };
+}
+
+macro_rules! deny_permissions_and_log_actions_in_thread {
+    ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => {
+        std::thread::Builder::new()
+            .name("fake_screen_thread".to_string())
+            .spawn({
+                let log = $arc_mutex_log.clone();
+                let mut exit_event_count = 0;
+                let cache_path = $cache_path.clone();
+                let plugin_thread_sender = $plugin_thread_sender.clone();
+                move || loop {
+                    let (event, _err_ctx) = $receiver
+                        .recv()
+                        .expect("failed to receive event on channel");
+                    match event {
+                        $exit_event(..) => {
+                            exit_event_count += 1;
+                            log.lock().unwrap().push(event);
+                            if exit_event_count == $exit_after_count {
+                                break;
+                            }
+                        },
+                        ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => {
+                            let _ = plugin_thread_sender.send(
+                                PluginInstruction::PermissionRequestResult(
+                                    0,
+                                    Some($client_id),
+                                    plugin_permission.permissions,
+                                    PermissionStatus::Denied,
+                                    Some(cache_path.clone()),
+                                ),
+                            );
+                            break;
+                        },
+                        _ => {
+                            log.lock().unwrap().push(event);
+                        },
+                    }
+                }
+            })
+            .unwrap()
+    };
+}
+
+macro_rules! grant_permissions_and_log_actions_in_thread_naked_variant {
+    ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr, $permission_type:expr, $cache_path:expr, $plugin_thread_sender:expr, $client_id:expr ) => {
+        std::thread::Builder::new()
+            .name("fake_screen_thread".to_string())
+            .spawn({
+                let log = $arc_mutex_log.clone();
+                let mut exit_event_count = 0;
+                let cache_path = $cache_path.clone();
+                let plugin_thread_sender = $plugin_thread_sender.clone();
+                move || loop {
+                    let (event, _err_ctx) = $receiver
+                        .recv()
+                        .expect("failed to receive event on channel");
+                    match event {
+                        $exit_event => {
+                            exit_event_count += 1;
+                            log.lock().unwrap().push(event);
+                            if exit_event_count == $exit_after_count {
+                                break;
+                            }
+                        },
+                        ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => {
+                            if plugin_permission.permissions.contains($permission_type) {
+                                let _ = plugin_thread_sender.send(
+                                    PluginInstruction::PermissionRequestResult(
+                                        0,
+                                        Some($client_id),
+                                        plugin_permission.permissions,
+                                        PermissionStatus::Granted,
+                                        Some(cache_path.clone()),
+                                    ),
+                                );
+                            } else {
+                                let _ = plugin_thread_sender.send(
+                                    PluginInstruction::PermissionRequestResult(
+                                        0,
+                                        Some($client_id),
+                                        plugin_permission.permissions,
+                                        PermissionStatus::Denied,
+                                        Some(cache_path.clone()),
+                                    ),
+                                );
+                            }
+                        },
+                        _ => {
+                            log.lock().unwrap().push(event);
+                        },
+                    }
+                }
+            })
+            .unwrap()
+    };
+}
+
 macro_rules! log_actions_in_thread_naked_variant {
     ( $arc_mutex_log:expr, $exit_event:path, $receiver:expr, $exit_after_count:expr ) => {
         std::thread::Builder::new()
@@ -167,14 +319,14 @@ fn create_plugin_thread_with_server_receiver(
 ) -> (
     SenderWithContext<PluginInstruction>,
     Receiver<(ServerInstruction, ErrorContext)>,
+    Receiver<(ScreenInstruction, ErrorContext)>,
     Box<dyn FnMut()>,
 ) {
     let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from("."));
     let (to_server, server_receiver): ChannelWithContext<ServerInstruction> = channels::bounded(50);
     let to_server = SenderWithContext::new(to_server);
 
-    let (to_screen, _screen_receiver): ChannelWithContext<ScreenInstruction> =
-        channels::unbounded();
+    let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded();
     let to_screen = SenderWithContext::new(to_screen);
 
     let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> = channels::unbounded();
@@ -240,7 +392,12 @@ fn create_plugin_thread_with_server_receiver(
                                                                        // the plugin cache
         }
     };
-    (to_plugin, server_receiver, Box::new(teardown))
+    (
+        to_plugin,
+        server_receiver,
+        screen_receiver,
+        Box::new(teardown),
+    )
 }
 
 fn create_plugin_thread_with_pty_receiver(
@@ -248,6 +405,7 @@ fn create_plugin_thread_with_pty_receiver(
 ) -> (
     SenderWithContext<PluginInstruction>,
     Receiver<(PtyInstruction, ErrorContext)>,
+    Receiver<(ScreenInstruction, ErrorContext)>,
     Box<dyn FnMut()>,
 ) {
     let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from("."));
@@ -255,8 +413,7 @@ fn create_plugin_thread_with_pty_receiver(
         channels::bounded(50);
     let to_server = SenderWithContext::new(to_server);
 
-    let (to_screen, _screen_receiver): ChannelWithContext<ScreenInstruction> =
-        channels::unbounded();
+    let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded();
     let to_screen = SenderWithContext::new(to_screen);
 
     let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> = channels::unbounded();
@@ -322,7 +479,7 @@ fn create_plugin_thread_with_pty_receiver(
                                                                        // the plugin cache
         }
     };
-    (to_plugin, pty_receiver, Box::new(teardown))
+    (to_plugin, pty_receiver, screen_receiver, Box::new(teardown))
 }
 
 lazy_static! {
@@ -344,6 +501,9 @@ pub fn load_new_plugin_from_hd() {
     // message (this is what the fixture plugin does)
     // we then listen on our mock screen receiver to make sure we got a PluginBytes instruction
     // that contains said render, and assert against it
+    let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None);
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -359,11 +519,15 @@ pub fn load_new_plugin_from_hd() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::PluginBytes,
         screen_receiver,
-        2
+        2,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -403,8 +567,12 @@ pub fn load_new_plugin_from_hd() {
 #[test]
 #[ignore]
 pub fn plugin_workers() {
+    let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+                                          // destructor removes the directory
     let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None);
     let plugin_should_float = Some(false);
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let plugin_title = Some("test_plugin".to_owned());
     let run_plugin = RunPlugin {
         _allow_exec_host_cmd: false,
@@ -418,11 +586,15 @@ pub fn plugin_workers() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::PluginBytes,
         screen_receiver,
-        3
+        3,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -465,8 +637,12 @@ pub fn plugin_workers() {
 #[test]
 #[ignore]
 pub fn plugin_workers_persist_state() {
+    let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+                                          // destructor removes the directory
     let (plugin_thread_sender, screen_receiver, mut teardown) = create_plugin_thread(None);
     let plugin_should_float = Some(false);
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let plugin_title = Some("test_plugin".to_owned());
     let run_plugin = RunPlugin {
         _allow_exec_host_cmd: false,
@@ -480,11 +656,15 @@ pub fn plugin_workers_persist_state() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::PluginBytes,
         screen_receiver,
-        5
+        5,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -603,6 +783,7 @@ pub fn switch_to_mode_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -619,11 +800,82 @@ pub fn switch_to_mode_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ChangeMode,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
+
+    let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+    let _ = plugin_thread_sender.send(PluginInstruction::Load(
+        plugin_should_float,
+        plugin_title,
+        run_plugin,
+        tab_index,
+        client_id,
+        size,
+    ));
+    let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+        None,
+        Some(client_id),
+        Event::Key(Key::Char('a')), // this triggers a SwitchToMode(Tab) command in the fixture
+                                    // plugin
+    )]));
+    std::thread::sleep(std::time::Duration::from_millis(100));
+    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    teardown();
+    let switch_to_mode_event = received_screen_instructions
+        .lock()
+        .unwrap()
+        .iter()
+        .find_map(|i| {
+            if let ScreenInstruction::ChangeMode(..) = i {
+                Some(i.clone())
+            } else {
+                None
+            }
+        })
+        .clone();
+    assert_snapshot!(format!("{:#?}", switch_to_mode_event));
+}
+
+#[test]
+#[ignore]
+pub fn switch_to_mode_plugin_command_permission_denied() {
+    let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+                                          // destructor removes the directory
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, screen_receiver, mut teardown) =
+        create_plugin_thread(Some(plugin_host_folder));
+    let plugin_should_float = Some(false);
+    let plugin_title = Some("test_plugin".to_owned());
+    let run_plugin = RunPlugin {
+        _allow_exec_host_cmd: false,
+        location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+        configuration: Default::default(),
+    };
+    let tab_index = 1;
+    let client_id = 1;
+    let size = Size {
+        cols: 121,
+        rows: 20,
+    };
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let screen_thread = deny_permissions_and_log_actions_in_thread!(
+        received_screen_instructions,
+        ScreenInstruction::ChangeMode,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -665,6 +917,7 @@ pub fn new_tabs_with_layout_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -681,11 +934,15 @@ pub fn new_tabs_with_layout_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::NewTab,
         screen_receiver,
-        2
+        2,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -741,6 +998,7 @@ pub fn new_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -757,11 +1015,15 @@ pub fn new_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::NewTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -803,6 +1065,7 @@ pub fn go_to_next_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -819,11 +1082,15 @@ pub fn go_to_next_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::SwitchTabNext,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -864,6 +1131,7 @@ pub fn go_to_previous_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -880,11 +1148,15 @@ pub fn go_to_previous_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::SwitchTabPrev,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -925,6 +1197,7 @@ pub fn resize_focused_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -941,11 +1214,15 @@ pub fn resize_focused_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::Resize,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -986,6 +1263,7 @@ pub fn resize_focused_pane_with_direction_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1002,11 +1280,15 @@ pub fn resize_focused_pane_with_direction_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::Resize,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1047,6 +1329,7 @@ pub fn focus_next_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1063,11 +1346,15 @@ pub fn focus_next_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::FocusNextPane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1108,6 +1395,7 @@ pub fn focus_previous_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1124,11 +1412,15 @@ pub fn focus_previous_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::FocusPreviousPane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1169,6 +1461,7 @@ pub fn move_focus_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1185,11 +1478,15 @@ pub fn move_focus_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::MoveFocusLeft,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1230,6 +1527,7 @@ pub fn move_focus_or_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1246,11 +1544,15 @@ pub fn move_focus_or_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::MoveFocusLeftOrPreviousTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1291,6 +1593,7 @@ pub fn edit_scrollback_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1307,11 +1610,15 @@ pub fn edit_scrollback_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::EditScrollback,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1352,6 +1659,7 @@ pub fn write_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1368,11 +1676,15 @@ pub fn write_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::WriteCharacter,
         screen_receiver,
-        1
+        1,
+        &PermissionType::WriteToStdin,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1413,6 +1725,7 @@ pub fn write_chars_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1429,11 +1742,15 @@ pub fn write_chars_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::WriteCharacter,
         screen_receiver,
-        1
+        1,
+        &PermissionType::WriteToStdin,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1474,6 +1791,7 @@ pub fn toggle_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1490,11 +1808,15 @@ pub fn toggle_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ToggleTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1535,6 +1857,7 @@ pub fn move_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1551,11 +1874,15 @@ pub fn move_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::MovePane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1596,6 +1923,7 @@ pub fn move_pane_with_direction_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1612,11 +1940,15 @@ pub fn move_pane_with_direction_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::MovePaneLeft,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1657,6 +1989,7 @@ pub fn clear_screen_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1673,11 +2006,16 @@ pub fn clear_screen_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ClearScreen,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1689,6 +2027,7 @@ pub fn clear_screen_plugin_command() {
         client_id,
         size,
     ));
+    std::thread::sleep(std::time::Duration::from_millis(100));
     let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
         None,
         Some(client_id),
@@ -1718,6 +2057,7 @@ pub fn scroll_up_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1734,11 +2074,16 @@ pub fn scroll_up_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ScrollUp,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1779,6 +2124,7 @@ pub fn scroll_down_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1795,11 +2141,15 @@ pub fn scroll_down_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ScrollDown,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1840,6 +2190,7 @@ pub fn scroll_to_top_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1856,11 +2207,15 @@ pub fn scroll_to_top_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ScrollToTop,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1901,6 +2256,7 @@ pub fn scroll_to_bottom_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1917,11 +2273,15 @@ pub fn scroll_to_bottom_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ScrollToBottom,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -1962,6 +2322,7 @@ pub fn page_scroll_up_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -1978,11 +2339,15 @@ pub fn page_scroll_up_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::PageScrollUp,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2023,6 +2388,7 @@ pub fn page_scroll_down_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2039,11 +2405,15 @@ pub fn page_scroll_down_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::PageScrollDown,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2084,6 +2454,7 @@ pub fn toggle_focus_fullscreen_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2100,11 +2471,15 @@ pub fn toggle_focus_fullscreen_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ToggleActiveTerminalFullscreen,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2145,6 +2520,7 @@ pub fn toggle_pane_frames_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2161,11 +2537,15 @@ pub fn toggle_pane_frames_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread_naked_variant!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
         received_screen_instructions,
         ScreenInstruction::TogglePaneFrames,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2206,6 +2586,7 @@ pub fn toggle_pane_embed_or_eject_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2222,11 +2603,15 @@ pub fn toggle_pane_embed_or_eject_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::TogglePaneEmbedOrFloating,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2267,6 +2652,7 @@ pub fn undo_rename_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2283,11 +2669,15 @@ pub fn undo_rename_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::UndoRenamePane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2328,6 +2718,7 @@ pub fn close_focus_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2344,11 +2735,15 @@ pub fn close_focus_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::CloseFocusedPane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2389,6 +2784,7 @@ pub fn toggle_active_tab_sync_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2405,11 +2801,15 @@ pub fn toggle_active_tab_sync_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ToggleActiveSyncTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2450,6 +2850,7 @@ pub fn close_focused_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2466,11 +2867,15 @@ pub fn close_focused_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::CloseTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2511,6 +2916,7 @@ pub fn undo_rename_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2527,11 +2933,15 @@ pub fn undo_rename_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::UndoRenameTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2572,6 +2982,7 @@ pub fn previous_swap_layout_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2588,11 +2999,15 @@ pub fn previous_swap_layout_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::PreviousSwapLayout,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2633,6 +3048,7 @@ pub fn next_swap_layout_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2649,11 +3065,15 @@ pub fn next_swap_layout_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::NextSwapLayout,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2694,6 +3114,7 @@ pub fn go_to_tab_name_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2710,11 +3131,15 @@ pub fn go_to_tab_name_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::GoToTabName,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2755,6 +3180,7 @@ pub fn focus_or_create_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2771,11 +3197,15 @@ pub fn focus_or_create_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::GoToTabName,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2816,6 +3246,7 @@ pub fn go_to_tab() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2832,11 +3263,15 @@ pub fn go_to_tab() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::GoToTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2877,6 +3312,7 @@ pub fn start_or_reload_plugin() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -2893,11 +3329,15 @@ pub fn start_or_reload_plugin() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::StartOrReloadPluginPane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -2938,7 +3378,8 @@ pub fn quit_zellij_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, server_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, server_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -2954,12 +3395,23 @@ pub fn quit_zellij_plugin_command() {
         rows: 20,
     };
     let received_server_instruction = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let server_thread = log_actions_in_thread!(
         received_server_instruction,
         ServerInstruction::ClientExit,
         server_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -2976,7 +3428,7 @@ pub fn quit_zellij_plugin_command() {
         Event::Key(Key::Char('8')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    server_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
     let new_tab_event = received_server_instruction
         .lock()
@@ -2999,7 +3451,8 @@ pub fn detach_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, server_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, server_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3015,12 +3468,23 @@ pub fn detach_plugin_command() {
         rows: 20,
     };
     let received_server_instruction = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let server_thread = log_actions_in_thread!(
         received_server_instruction,
         ServerInstruction::DetachSession,
         server_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3037,7 +3501,7 @@ pub fn detach_plugin_command() {
         Event::Key(Key::Char('l')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    server_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
     let new_tab_event = received_server_instruction
         .lock()
@@ -3060,7 +3524,8 @@ pub fn open_file_floating_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3075,13 +3540,24 @@ pub fn open_file_floating_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3098,9 +3574,9 @@ pub fn open_file_floating_plugin_command() {
         Event::Key(Key::Ctrl('h')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3121,7 +3597,8 @@ pub fn open_file_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3136,13 +3613,24 @@ pub fn open_file_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3159,9 +3647,9 @@ pub fn open_file_plugin_command() {
         Event::Key(Key::Ctrl('g')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3182,7 +3670,8 @@ pub fn open_file_with_line_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3197,14 +3686,26 @@ pub fn open_file_with_line_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
 
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
+
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
         plugin_should_float,
@@ -3220,9 +3721,9 @@ pub fn open_file_with_line_plugin_command() {
         Event::Key(Key::Ctrl('i')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3243,7 +3744,8 @@ pub fn open_file_with_line_floating_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3258,13 +3760,24 @@ pub fn open_file_with_line_floating_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3281,9 +3794,9 @@ pub fn open_file_with_line_floating_plugin_command() {
         Event::Key(Key::Ctrl('j')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3304,7 +3817,8 @@ pub fn open_terminal_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3319,13 +3833,24 @@ pub fn open_terminal_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3342,9 +3867,9 @@ pub fn open_terminal_plugin_command() {
         Event::Key(Key::Ctrl('k')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3365,7 +3890,8 @@ pub fn open_terminal_floating_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3380,13 +3906,24 @@ pub fn open_terminal_floating_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3403,9 +3940,9 @@ pub fn open_terminal_floating_plugin_command() {
         Event::Key(Key::Ctrl('l')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3426,7 +3963,8 @@ pub fn open_command_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3441,13 +3979,24 @@ pub fn open_command_pane_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3464,9 +4013,9 @@ pub fn open_command_pane_plugin_command() {
         Event::Key(Key::Ctrl('m')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3487,7 +4036,8 @@ pub fn open_command_pane_floating_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
-    let (plugin_thread_sender, pty_receiver, mut teardown) =
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+    let (plugin_thread_sender, pty_receiver, screen_receiver, mut teardown) =
         create_plugin_thread_with_pty_receiver(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
     let plugin_title = Some("test_plugin".to_owned());
@@ -3502,13 +4052,24 @@ pub fn open_command_pane_floating_plugin_command() {
         cols: 121,
         rows: 20,
     };
-    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
-        received_screen_instructions,
+    let received_pty_instructions = Arc::new(Mutex::new(vec![]));
+    let pty_thread = log_actions_in_thread!(
+        received_pty_instructions,
         PtyInstruction::SpawnTerminal,
         pty_receiver,
         1
     );
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+        received_screen_instructions,
+        ScreenInstruction::Exit,
+        screen_receiver,
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
+    );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
     let _ = plugin_thread_sender.send(PluginInstruction::Load(
@@ -3525,9 +4086,9 @@ pub fn open_command_pane_floating_plugin_command() {
         Event::Key(Key::Ctrl('n')), // this triggers the enent in the fixture plugin
     )]));
     std::thread::sleep(std::time::Duration::from_millis(100));
-    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    pty_thread.join().unwrap(); // this might take a while if the cache is cold
     teardown();
-    let new_tab_event = received_screen_instructions
+    let new_tab_event = received_pty_instructions
         .lock()
         .unwrap()
         .iter()
@@ -3548,6 +4109,7 @@ pub fn switch_to_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -3564,11 +4126,15 @@ pub fn switch_to_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::GoToTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -3731,6 +4297,7 @@ pub fn close_terminal_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -3747,11 +4314,15 @@ pub fn close_terminal_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ClosePane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -3792,6 +4363,7 @@ pub fn close_plugin_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -3808,11 +4380,15 @@ pub fn close_plugin_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::ClosePane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -3853,6 +4429,7 @@ pub fn focus_terminal_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -3869,11 +4446,15 @@ pub fn focus_terminal_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::FocusPaneWithId,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -3914,6 +4495,7 @@ pub fn focus_plugin_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -3930,11 +4512,15 @@ pub fn focus_plugin_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::FocusPaneWithId,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -3975,6 +4561,7 @@ pub fn rename_terminal_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -3991,11 +4578,15 @@ pub fn rename_terminal_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::RenamePane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -4036,6 +4627,7 @@ pub fn rename_plugin_pane_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -4052,11 +4644,15 @@ pub fn rename_plugin_pane_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::RenamePane,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -4097,6 +4693,7 @@ pub fn rename_tab_plugin_command() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -4113,11 +4710,15 @@ pub fn rename_tab_plugin_command() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::RenameTab,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -4158,6 +4759,7 @@ pub fn send_configuration_to_plugins() {
     let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
                                           // destructor removes the directory
     let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
     let (plugin_thread_sender, screen_receiver, mut teardown) =
         create_plugin_thread(Some(plugin_host_folder));
     let plugin_should_float = Some(false);
@@ -4183,11 +4785,15 @@ pub fn send_configuration_to_plugins() {
         rows: 20,
     };
     let received_screen_instructions = Arc::new(Mutex::new(vec![]));
-    let screen_thread = log_actions_in_thread!(
+    let screen_thread = grant_permissions_and_log_actions_in_thread!(
         received_screen_instructions,
         ScreenInstruction::GoToTabName,
         screen_receiver,
-        1
+        1,
+        &PermissionType::ChangeApplicationState,
+        cache_path,
+        plugin_thread_sender,
+        client_id
     );
 
     let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
@@ -4224,3 +4830,226 @@ pub fn send_configuration_to_plugins() {
         .clone();
     assert_snapshot!(format!("{:#?}", go_to_tab_event));
 }
+
+#[test]
+#[ignore]
+pub fn request_plugin_permissions() {
+    let temp_folder = tempdir().unwrap();
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let (plugin_thread_sender, screen_receiver, mut teardown) =
+        create_plugin_thread(Some(plugin_host_folder));
+    let plugin_should_float = Some(false);
+    let plugin_title = Some("test_plugin".to_owned());
+    let run_plugin = RunPlugin {
+        _allow_exec_host_cmd: false,
+        location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+        configuration: Default::default(),
+    };
+    let tab_index = 1;
+    let client_id = 1;
+    let size = Size {
+        cols: 121,
+        rows: 20,
+    };
+    let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+    let screen_thread = log_actions_in_thread!(
+        received_screen_instructions,
+        ScreenInstruction::RequestPluginPermissions,
+        screen_receiver,
+        1
+    );
+
+    let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+    let _ = plugin_thread_sender.send(PluginInstruction::Load(
+        plugin_should_float,
+        plugin_title,
+        run_plugin,
+        tab_index,
+        client_id,
+        size,
+    ));
+    let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+        None,
+        Some(client_id),
+        Event::Key(Key::Ctrl('1')), // this triggers the enent in the fixture plugin
+    )]));
+    std::thread::sleep(std::time::Duration::from_millis(100));
+    screen_thread.join().unwrap(); // this might take a while if the cache is cold
+    teardown();
+    let new_tab_event = received_screen_instructions
+        .lock()
+        .unwrap()
+        .iter()
+        .find_map(|i| {
+            if let ScreenInstruction::RequestPluginPermissions(_, plugin_permission) = i {
+                Some(plugin_permission.permissions.clone())
+            } else {
+                None
+            }
+        })
+        .clone();
+    assert_snapshot!(format!("{:#?}", new_tab_event));
+}
+
+#[test]
+#[ignore]
+pub fn granted_permission_request_result() {
+    let temp_folder = tempdir().unwrap();
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+
+    let (plugin_thread_sender, screen_receiver, mut teardown) =
+        create_plugin_thread(Some(plugin_host_folder));
+    let plugin_should_float = Some(false);
+    let plugin_title = Some("test_plugin".to_owned());
+    let run_plugin = RunPlugin {
+        _allow_exec_host_cmd: false,
+        location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+        configuration: Default::default(),
+    };
+    let tab_index = 1;
+    let client_id = 1;
+    let size = Size {
+        cols: 121,
+        rows: 20,
+    };
+
+    // here we create a fake screen thread that will send a PermissionStatus::Granted
+    // message for every permission request it gets
+    let screen_thread = std::thread::Builder::new()
+        .name("fake_screen_thread".to_string())
+        .spawn({
+            let cache_path = cache_path.clone();
+            let plugin_thread_sender = plugin_thread_sender.clone();
+            move || loop {
+                let (event, _err_ctx) = screen_receiver
+                    .recv()
+                    .expect("failed to receive event on channel");
+                match event {
+                    ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => {
+                        let _ =
+                            plugin_thread_sender.send(PluginInstruction::PermissionRequestResult(
+                                0,
+                                Some(client_id),
+                                plugin_permission.permissions,
+                                PermissionStatus::Granted,
+                                Some(cache_path.clone()),
+                            ));
+                        break;
+                    },
+                    ScreenInstruction::Exit => {
+                        break;
+                    },
+                    _ => {},
+                }
+            }
+        })
+        .unwrap();
+
+    let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+    let _ = plugin_thread_sender.send(PluginInstruction::Load(
+        plugin_should_float,
+        plugin_title,
+        run_plugin.clone(),
+        tab_index,
+        client_id,
+        size,
+    ));
+    let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+        None,
+        Some(client_id),
+        Event::Key(Key::Ctrl('1')), // this triggers the enent in the fixture plugin
+    )]));
+    screen_thread.join().unwrap();
+    teardown();
+
+    let permission_cache = PermissionCache::from_path_or_default(Some(cache_path));
+    let mut permissions = permission_cache
+        .get_permissions(run_plugin.location.to_string())
+        .clone();
+    let permissions = permissions.as_mut().map(|p| {
+        let mut permissions = p.clone();
+        permissions.sort_unstable();
+        permissions
+    });
+
+    assert_snapshot!(format!("{:#?}", permissions));
+}
+
+#[test]
+#[ignore]
+pub fn denied_permission_request_result() {
+    let temp_folder = tempdir().unwrap();
+    let plugin_host_folder = PathBuf::from(temp_folder.path());
+    let cache_path = plugin_host_folder.join("permissions_test.kdl");
+
+    let (plugin_thread_sender, screen_receiver, mut teardown) =
+        create_plugin_thread(Some(plugin_host_folder));
+    let plugin_should_float = Some(false);
+    let plugin_title = Some("test_plugin".to_owned());
+    let run_plugin = RunPlugin {
+        _allow_exec_host_cmd: false,
+        location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+        configuration: Default::default(),
+    };
+    let tab_index = 1;
+    let client_id = 1;
+    let size = Size {
+        cols: 121,
+        rows: 20,
+    };
+
+    // here we create a fake screen thread that will send a PermissionStatus::Granted
+    // message for every permission request it gets
+    let screen_thread = std::thread::Builder::new()
+        .name("fake_screen_thread".to_string())
+        .spawn({
+            let cache_path = cache_path.clone();
+            let plugin_thread_sender = plugin_thread_sender.clone();
+            move || loop {
+                let (event, _err_ctx) = screen_receiver
+                    .recv()
+                    .expect("failed to receive event on channel");
+                match event {
+                    ScreenInstruction::RequestPluginPermissions(_, plugin_permission) => {
+                        let _ =
+                            plugin_thread_sender.send(PluginInstruction::PermissionRequestResult(
+                                0,
+                                Some(client_id),
+                                plugin_permission.permissions,
+                                PermissionStatus::Denied,
+                                Some(cache_path.clone()),
+                            ));
+                        break;
+                    },
+                    ScreenInstruction::Exit => {
+                        break;
+                    },
+                    _ => {},
+                }
+            }
+        })
+        .unwrap();
+
+    let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+    let _ = plugin_thread_sender.send(PluginInstruction::Load(
+        plugin_should_float,
+        plugin_title,
+        run_plugin.clone(),
+        tab_index,
+        client_id,
+        size,
+    ));
+    let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+        None,
+        Some(client_id),
+        Event::Key(Key::Ctrl('1')), // this triggers the enent in the fixture plugin
+    )]));
+    screen_thread.join().unwrap();
+    teardown();
+
+    let permission_cache = PermissionCache::from_path_or_default(Some(cache_path));
+    let permissions = permission_cache.get_permissions(run_plugin.location.to_string());
+
+    assert_snapshot!(format!("{:#?}", permissions));
+}
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap
new file mode 100644
index 0000000000..d7dfeb16db
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__denied_permission_request_result.snap
@@ -0,0 +1,7 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+expression: "format!(\"{:#?}\", permissions)"
+---
+Some(
+    [],
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap
new file mode 100644
index 0000000000..beaa4adc77
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__granted_permission_request_result.snap
@@ -0,0 +1,15 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+assertion_line: 4864
+expression: "format!(\"{:#?}\", permissions)"
+---
+Some(
+    [
+        ReadApplicationState,
+        ChangeApplicationState,
+        OpenFiles,
+        RunCommands,
+        OpenTerminalsOrPlugins,
+        WriteToStdin,
+    ],
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap
new file mode 100644
index 0000000000..23fa6c224e
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__request_plugin_permissions.snap
@@ -0,0 +1,17 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+assertion_line: 4767
+expression: "format!(\"{:#?}\", new_tab_event)"
+---
+Some(
+    [
+        ChangeApplicationState,
+        ReadApplicationState,
+        ReadApplicationState,
+        ChangeApplicationState,
+        OpenFiles,
+        RunCommands,
+        OpenTerminalsOrPlugins,
+        WriteToStdin,
+    ],
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap
new file mode 100644
index 0000000000..57afe877d1
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_to_mode_plugin_command_permission_denied.snap
@@ -0,0 +1,6 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+assertion_line: 903
+expression: "format!(\"{:#?}\", switch_to_mode_event)"
+---
+None
diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
index 80a6776e9f..ff13cefd08 100755
Binary files a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm and b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm differ