Plugin System
Lash supports two plugin flavours:
- Native plugins —
.lashfiles loaded in-process on session start. Best for typed shell shortcuts (gst,gco, …), wrapper commands, and argument-completion types. Zero IPC overhead. - External plugins — standalone processes that communicate with the backend over Unix domain sockets. Best for cross-language integrations (csv, yaml parsers) and persistent daemons. Crash-isolated, language-agnostic.
Pick native for anything that can be expressed as lash code. Pick external when you need another runtime (Python, Go, Rust) or long-running state.
Native plugins
Section titled “Native plugins”Native plugins are plain .lash files. Every top-level fn and type declaration is registered into the session scope.
Discovery order
Section titled “Discovery order”Lash walks three layers in order, with later layers overriding earlier same-named declarations:
- Bundled — compiled into the binary via string-import. Currently:
git.lash. - System —
/usr/share/lash/plugins/*.lash(for package maintainers). - User —
~/.lash/plugins/*.lash.
Files are sorted alphabetically within each layer. Loading is done in a fresh scope; errors are emitted but don’t abort other plugins.
Declaration shapes
Section titled “Declaration shapes”/// Push the current branch and set upstream.fn gpsup(...args: [string]) { git push --set-upstream origin HEAD ...args}
/// A local or remote git branch name.type GitBranch { cache = 2s values(query) { `git branch -a --format=%(refname:short)` } validate(x) { x in values("") } // optional — derived if omitted}fnaccepts typed params, union types (A | B), list types ([T]), and a single trailing rest param (...args: [T]).typemust definevalues(query) -> list.validate(x) -> boolis optional; if absent lash derives it asx in values("").cache = <duration>memoizesvalues(query)per-query for the given TTL. Units:ms,s,m,h,d. Errors are never cached.- Doc comments (
///or/** */) on fn, type, or parameter attach to the AST node and feed--helpoutput and LSP hover.
fn main and script dispatch
Section titled “fn main and script dispatch”When a .lash file is executed (not loaded as a plugin), a top-level fn main(...) acts as the entry point:
/// Greet someone by name.fn main(name: string, count: int) { for _ in 1..count { echo "Hello, $name!" }}CLI args are coerced through the typed signature. Missing required args or failed coercion emits a user-friendly error plus auto-generated help:
Error: Missing required argument 'count' (int)
greet.lash — Greet someone by name.
Arguments: name (string) count (int)Scripts without an explicit fn main receive CLI args through an implicit args list variable. $0 resolves to the script path.
$1..$N, $@, $*, and $# are not supported — typed parameters replace them.
Inspection
Section titled “Inspection”lash --list-pluginsPrints native plugins (fn signatures with doc summary, types with method list and cache TTL) alongside external ones.
External plugins
Section titled “External plugins”External plugins are standalone processes that communicate with the backend over Unix domain sockets. They are language-agnostic, crash-isolated, and independently deployable. The remainder of this page documents their protocol.
Lifecycle
Section titled “Lifecycle”- Backend reads
~/.lash/plugins.confon startup. - For each plugin, spawns the process with a socket path.
- Plugin opens socket, performs handshake.
- During execution, plugin methods are dispatched over the socket.
- On shutdown, plugins receive a
terminatemessage.
Registration
Section titled “Registration”In ~/.lash/plugins.conf:
[plugins]powerline = builtin:powerlinehistory = builtin:historysession = builtin:sessioncompletions = builtin:completionshighlight = builtin:highlightfrequency = builtin:frequencybanner = builtin:bannercsv = /usr/local/bin/lash-csvyaml = ~/.lash/plugins/yaml-pluginBuilt-in plugins are spawned as lash --plugin <name> <socket-path>. External plugins are spawned directly with the socket path as the first argument.
The audit plugin is always loaded regardless of plugins.conf contents, since it provides the audit and profile builtin commands.
lash --list-plugins sends plugin.describe to each plugin and displays their names, descriptions, hooks, methods, and completions.
Handshake Protocol
Section titled “Handshake Protocol”All plugin communication uses JSON lines over Unix domain socket at ~/.lash/plugins/<name>.sock.
Backend to plugin:
{"type":"handshake","version":"1.0"}Plugin to backend:
{ "type": "handshake_ok", "methods": ["csv"], "hooks": ["pipeline.post_execute", "plugin.describe"], "completions": ["kubectl"], "hook_config": [ {"event": "input.key", "keys": ["ctrl+c", "escape"], "mode": "blocking"}, {"event": "prompt.render", "mode": "fire_and_forget"} ]}| Field | Description |
|---|---|
methods | Method names the plugin provides for functional chains. |
hooks | Hook events subscribed to (default mode: blocking). |
completions | Command prefixes the plugin completes (["*"] for all). |
hook_config | (Optional) Per-event mode and key filter overrides. |
Method Call Protocol
Section titled “Method Call Protocol”Request (backend to plugin):
{"type":"method_call","method":"audit","args":["init"],"context":{"profile":"default","session_id":"s_123","audit_access":"true"}}| Field | Description |
|---|---|
method | Method name declared in handshake. |
args | Array of string arguments from the command line. |
context | Session context: active profile, session ID, capabilities. |
Success response:
{"type":"method_response","exit_code":0,"set_profile":"user"}| Field | Description |
|---|---|
exit_code | Exit code for the command (0 = success). |
set_profile | (Optional) Requests the daemon apply a profile change to the session. |
During a method call, the plugin may send inline output and prompt messages before the final response (see Interactive Prompt Protocol).
Error response:
{"type":"error","problem":"CSV parse failed at row 3","suggestion":"Check for unescaped commas"}Completion Protocol
Section titled “Completion Protocol”Request:
{"type":"complete_request","text":"git ch","cursor":6,"context":{"cwd":"/home/user","env":{"PATH":"/usr/bin"},"aliases":{"ll":"ls -la"},"scope_vars":["MY_VAR"]}}Response:
{"type":"complete_response","prefix":"ch","items":["checkout","cherry-pick"]}Highlight Protocol
Section titled “Highlight Protocol”Request:
{"type":"highlight_request","text":"git commit -m 'msg'","context":{"aliases":{"ll":"ls -la"},"builtins":["exit","jobs"]}}Response:
{"type":"highlight_response","tokens":[{"offset":0,"length":3,"style":"\u001b[32m"}]}Plugin Describe
Section titled “Plugin Describe”Triggered via the plugin.describe hook:
{"type":"hook","event":"plugin.describe","session_id":"","timestamp":"...","data":{}}Response:
{"action":"modify","data":{"description":"Human-readable plugin description"}}Interactive Prompt Protocol
Section titled “Interactive Prompt Protocol”Plugins can request user input during a method call. When a plugin sends a prompt message, the backend relays it to the frontend, collects the response, and sends it back to the plugin. The password never touches other plugins — it flows only through the requesting plugin’s socket.
Three prompt types are available:
Password Prompt
Section titled “Password Prompt”Collects hidden input (echo disabled). Used for sensitive data like the audit password.
Plugin to backend:
{"type":"prompt_password","text":"Audit password: "}Backend to plugin (after user responds):
{"type":"prompt_response","value":"hunter2"}Text Input Prompt
Section titled “Text Input Prompt”Collects visible text input. Used for free-form answers.
Plugin to backend:
{"type":"prompt_input","text":"Default profile name: "}Backend to plugin:
{"type":"prompt_response","value":"developer"}Select Prompt
Section titled “Select Prompt”Presents a numbered list the user can navigate with arrow keys. Used for choosing from a fixed set of options.
Plugin to backend:
{"type":"prompt_select","text":"Select a default profile:","options":["default","script","ci","restricted"]}Backend to plugin:
{"type":"prompt_response","value":"user"}The response contains the selected option’s text, not its index.
Output Messages
Section titled “Output Messages”During a method call, plugins can send text to display on the user’s terminal:
Plugin to backend:
{"type":"output","text":"Audit initialized.\n"}The backend relays this to the frontend as terminal output.
Terminate
Section titled “Terminate”{"type":"terminate"}Priority and Conflicts
Section titled “Priority and Conflicts”Built-in methods always take priority over plugin methods. Two plugins providing the same method name is a startup error.
Crash Handling
Section titled “Crash Handling”If a plugin process dies, the backend reports it via a Problem/Suggestion error and the chain fails at the plugin method step. Plugins run as separate processes, so a crashed plugin cannot take down the shell.