Plugins are external executables that hook into ez-workspaces lifecycle events. They receive a JSON request on stdin and return a JSON response on stdout. Any language works — shell, Python, Go, Rust, etc.
~/.config/ez/plugins/my-plugin/
manifest.toml # Plugin metadata
my-plugin-exec # Executable (any language)
name = "my-plugin"
version = "0.1.0"
description = "What this plugin does"
hooks = ["on_session_create", "on_session_delete", "on_session_enter", "on_view", "on_view_select"]
executable = "my-plugin-exec"
# Register a custom view (appears as a keybind in the browser)
[[views]]
name = "my-view"
key = "ctrl-a"
label = "my view"
contexts = ["session", "repo", "owner", "workspace", "tree", "label"]
# Register action keybinds on selected items
[[binds]]
key = "alt-x"
name = "my_action"
label = "do something"
contexts = ["session"]
# Declare user-facing configuration options
[[config_schema]]
name = "auto_run"
type = "bool"
default = false
description = "Run automatically on session enter"| Hook | When | Session in payload? |
|---|---|---|
on_session_create |
After session metadata created | Yes |
on_session_delete |
Before session metadata removed | Yes |
on_session_enter |
User enters a session | Yes |
on_session_exit |
User exits a session | Yes |
on_session_rename |
Session renamed | Yes |
on_session_sync |
User requests sync | Yes |
on_repo_clone |
After repo cloned + registered | No |
on_repo_remove |
Before repo unregistered | No |
on_plugin_init |
Plugin enabled | No |
on_plugin_deinit |
Plugin disabled | No |
on_view |
User switches to plugin view | No |
on_view_select |
User selects item in plugin view | No |
on_name_resolve |
Name builder requests name resolution from a URL | No |
{
"hook": "on_session_create",
"repo": {
"id": "personal-my-repo",
"path": "/home/user/workspace/personal/my-repo",
"remote_url": "git@github.com:user/my-repo.git",
"default_branch": "main"
},
"session": {
"id": "uuid-here",
"name": "feature-auth",
"parent_id": null,
"path": null,
"env": {},
"plugin_state": {},
"is_default": false
},
"config": {
"plugin_state": {},
"user_config": {}
},
"view_context": null,
"bind_context": null
}sessionisnullfor repo-level hooks and view hooksconfig.plugin_statecarries this plugin's per-repo state from previous invocationsconfig.user_configcarries user-facing settings from[plugin_settings.<name>]in config.tomlview_contextis present foron_viewandon_view_selecthooks (containsview_name,selected_value,selected_display)bind_contextis present foron_bindhooks (containsname,key,view,selected_value,selected_display)name_resolve_contextis present foron_name_resolvehooks (containsraw_url,candidate_name)
{
"success": true,
"error": null,
"session_mutations": {
"path": "/path/to/worktree",
"env": { "MY_VAR": "value" },
"plugin_state": { "my_key": "my_value" }
},
"repo_mutations": {
"plugin_state": { "repo_key": "repo_value" }
},
"shell_commands": ["echo 'hello'"],
"post_shell_commands": ["tmux switch-client -t mysession"],
"cd_target": "/path/to/directory",
"view_items": [
{ "display": " Item 1", "value": "item-1" },
{ "display": " Item 2", "value": "item-2" }
],
"view_prompt": "my items",
"view_preview_cmd": "cat {1}/README.md"
}- All fields except
successare optional session_mutationsandrepo_mutationsuse patch semantics (only included fields are merged)shell_commandsare executed inside ez (before exit)post_shell_commandsare executed in the user's shell after ez exits — use for commands that need the terminal (e.g.,tmux switch-client)cd_targetoverrides the directory the shell wrapper willcdintoview_items,view_prompt,view_preview_cmdare returned byon_viewhooks to provide items for plugin viewsresolved_nameis returned byon_name_resolvehooks to provide the resolved name (e.g. a branch name derived from a PR URL)erroris a string message ifsuccessis false- Plugins have a 30-second timeout by default (configurable via
plugin_timeoutin config)
Plugins can register custom views that appear as keybinds alongside the built-in views (Tree, Workspace, Repo, Owner, Label). When the user presses the view key, ez calls the plugin's on_view hook, renders the returned items in fzf, and calls on_view_select when the user picks one.
- User presses the plugin's view key (e.g.,
Ctrl-a) - ez calls the plugin with
on_viewhook - Plugin returns
view_items(display + value pairs), optionalview_promptandview_preview_cmd - ez renders items in fzf with all view-switch keys active
- User selects an item (or switches to another view)
- ez calls the plugin with
on_view_selecthook, passing the selected item inview_context - Plugin returns
post_shell_commandsand/orcd_target - ez writes post commands and exits; the shell wrapper executes them
If a plugin view key conflicts with a core keybind (e.g., ctrl-t is already used by Tree view), the core keybind wins and the plugin view is skipped. A warning is logged in debug mode. Choose a non-conflicting key or let users remap core keybinds in [keybinds].
The on_name_resolve hook fires during the interactive name builder when a mode needs to resolve a URL into a session name. Currently used by the GitHub PR mode: after the user pastes a PR URL, ez extracts pr<number> as a candidate name, then calls enabled plugins with on_name_resolve to optionally resolve a better name (e.g. the PR's branch name).
- User pastes a URL in the name builder (e.g.
https://github.com/user/repo/pull/42) - ez parses the URL and derives a
candidate_name(e.g.pr42) - ez calls all enabled plugins that handle
on_name_resolve - The plugin receives
name_resolve_contextwithraw_urlandcandidate_name - If the plugin returns
resolved_name, ez uses it as the session name - If the plugin fails or times out, ez falls back to
candidate_name
{
"hook": "on_name_resolve",
"name_resolve_context": {
"raw_url": "https://github.com/user/repo/pull/42",
"candidate_name": "pr42"
}
}{
"success": true,
"resolved_name": "fix-typo-in-readme"
}case "$HOOK" in
on_name_resolve)
RAW_URL=$(echo "$REQUEST" | jq -r '.name_resolve_context.raw_url // empty')
if echo "$RAW_URL" | grep -q 'github\.com'; then
BRANCH=$(gh pr view "$RAW_URL" --json headRefName -q '.headRefName' 2>/dev/null)
if [ -n "$BRANCH" ]; then
echo "{\"success\": true, \"resolved_name\": \"$BRANCH\"}"
else
echo '{"success": true}'
fi
else
echo '{"success": true}'
fi
;;
esacPlugins can declare user-facing configuration options via [[config_schema]] in the manifest. Users set these in config.toml:
[plugin_settings.my-plugin]
auto_run = true
custom_path = "/usr/local/bin/my-tool"The plugin receives these values in config.user_config on every hook invocation.
- For
on_session_enter/on_session_exit: plugin errors are warnings (non-fatal) - For
on_session_create/on_session_delete: plugin errors abort the operation - stderr output is logged as diagnostics
#!/usr/bin/env bash
set -euo pipefail
REQUEST=$(cat)
HOOK=$(echo "$REQUEST" | jq -r '.hook')
SESSION_NAME=$(echo "$REQUEST" | jq -r '.session.name // empty')
case "$HOOK" in
on_session_create)
echo "Creating session: $SESSION_NAME" >&2
echo '{"success": true}'
;;
*)
echo '{"success": true}'
;;
esac#!/usr/bin/env python3
import json, sys
request = json.load(sys.stdin)
hook = request["hook"]
session = request.get("session", {})
if hook == "on_session_enter":
response = {
"success": True,
"session_mutations": {
"env": {"MY_PROJECT_SESSION": session.get("name", "")}
}
}
else:
response = {"success": True}
json.dump(response, sys.stdout)- Place the plugin directory under
~/.config/ez/plugins/ - Ensure the executable has execute permissions
- Enable it:
ez plugin enable my-plugin