Developing Plugins
Zed plugins are directories containing a plugin.toml manifest. They can provide dock panels and title bar widgets.
Plugins run out of process.
- On macOS, Zed launches plugins through a host-managed sandbox with plugin-scoped writable paths. Development plugins still receive the writable paths they need for Cargo builds, plugin state, temp files, and localhost callbacks.
- On Linux, Zed launches plugins with
PR_SET_NO_NEW_PRIVS, a Landlock filesystem sandbox, and a seccomp filter that blocks mount, namespace, tracing, and kernel-instrumentation syscalls. - On Windows, Zed launches plugins inside AppContainers with plugin-scoped filesystem access, then attaches them to dedicated Job Objects so the host can tear down the full plugin process tree.
Plugins should treat secret storage as a host service, not a direct plugin responsibility. In particular, macOS sandboxed plugins should not write credentials directly to Keychain. Use the host secure-storage API exposed through gpui_plugin so the host process persists secrets on the plugin's behalf.
Plugin Features
Plugins can provide:
- Panels
- Title Bar Widgets
Developing a Plugin Locally
Before starting to develop a plugin for Zed, be sure to install Rust and Cargo.
When developing a plugin, you can use it in Zed without needing to publish it by installing it as a dev plugin.
From the Plugins page, click the Install Dev Plugin button (or the zed: install dev plugin action) and select the directory containing your plugin.
If you need to troubleshoot, check Zed.log (zed: open log) for additional output. For debug output, close and relaunch Zed from the command line with zed --foreground, which shows more verbose INFO-level logs.
Directory Structure of a Zed Plugin
A Zed plugin is a directory that contains a plugin.toml. This file must contain some basic information about the plugin:
id = "my-plugin"
name = "My Plugin"
version = "0.0.1"
schema_version = 1
authors = ["Your Name <[email protected]>"]
description = "Example plugin"
repository = "https://github.com/your-name/my-zed-plugin"
homepage = "https://github.com/your-name/my-zed-plugin"
entry = "my-plugin"
[[panels]]
id = "my-plugin-panel"
title = "My Plugin"
dock = "right"
tooltip = "Open the My Plugin panel."
activation = "on_demand"
[[titlebar_widgets]]
id = "my-plugin-widget"
title = "My Plugin"
tooltip = "Open the My Plugin panel."
side = "right"
priority = 100
opens_panel_id = "my-plugin-panel"
Panels can be placed in the left, right, or bottom dock, and can use either on_demand or on_startup activation. Title bar widgets can be placed on the left or right side. If opens_panel_id is set, clicking the widget opens the referenced plugin panel.
An example directory structure of a plugin is as follows:
my-plugin/
plugin.toml
Cargo.toml
src/
main.rs
Building Plugin UI
Plugin UI is written in Rust and runs in a separate process. To use the GPUI mirror runtime, add gpui_plugin and ui_plugin as dependencies and alias them to gpui and ui:
[package]
name = "my-plugin"
version = "0.0.1"
edition = "2021"
[features]
default = []
mirror = ["dep:gpui", "dep:ui"]
[[bin]]
name = "my-plugin"
path = "src/main.rs"
[dependencies]
anyhow = "1"
gpui = { package = "gpui_plugin", path = "../../crates/gpui_plugin", optional = true }
ui = { package = "ui_plugin", path = "../../crates/ui_plugin", optional = true }
This example assumes the plugin lives under the Zed repository's plugins/ directory. If your plugin lives elsewhere, update the dependency paths accordingly.
When Zed launches a Cargo-backed plugin, it runs cargo run --features mirror --bin <entry> from the plugin directory. If your plugin is not a Cargo project, entry can point to a relative executable path instead.
Within plugin code, use gpui::*; and use ui::*; work the same way as in-process UI code. The plugin runtime mirrors those APIs and sends the resulting element tree back to Zed.
In src/main.rs, register each panel and title bar widget declared in plugin.toml:
use anyhow::Result;
use gpui::*;
use ui::*;
struct MyPluginPanel;
impl Render for MyPluginPanel {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.p_4()
.gap_2()
.child(Label::new("Hello from a plugin"))
}
}
struct MyPluginWidget;
impl Render for MyPluginWidget {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
h_flex().gap_1().child(Label::new("My Plugin"))
}
}
fn main() -> Result<()> {
gpui::run(|plugin| {
plugin.register_panel("my-plugin-panel", |_cx| MyPluginPanel);
plugin.register_titlebar_widget("my-plugin-widget", |_cx| MyPluginWidget);
})
}
Each [[panels]] and [[titlebar_widgets]] entry in plugin.toml must also be registered in code.
Secure Storage
Plugins that need to persist secrets should use gpui_plugin::host_request with PluginHostRequest. This allows secrets to persist securely across sessions.
use gpui::{host_request, PluginHostRequest, PluginHostResponse};
fn load_secret() -> anyhow::Result<Option<String>> {
let response = host_request(PluginHostRequest::SecureStorageLoad {
key: "my-secret".to_string(),
})?;
let PluginHostResponse::SecureStorageLoad { value } = response else {
anyhow::bail!("unexpected secure storage response");
};
Ok(value)
}
fn save_secret(value: String) -> anyhow::Result<()> {
host_request(PluginHostRequest::SecureStorageStore {
key: "my-secret".to_string(),
value,
})?;
Ok(())
}
fn clear_secret() -> anyhow::Result<()> {
host_request(PluginHostRequest::SecureStorageClear {
key: "my-secret".to_string(),
})?;
Ok(())
}
The host scopes secure-storage entries to the plugin id, so keys only need to be unique within a single plugin.