Skip to content

Plugin System

Plugins are external processes that communicate with the backend over Unix domain sockets. They are language-agnostic, crash-isolated, and independently deployable.

  1. Backend reads ~/.lash/plugins.conf on startup.
  2. For each plugin, spawns the process with a socket path.
  3. Plugin opens socket, performs handshake.
  4. During execution, plugin methods are dispatched over the socket.
  5. On shutdown, plugins receive a terminate message.

In ~/.lash/plugins.conf:

[plugins]
powerline = builtin:powerline
history = builtin:history
session = builtin:session
completions = builtin:completions
highlight = builtin:highlight
frequency = builtin:frequency
banner = builtin:banner
csv = /usr/local/bin/lash-csv
yaml = ~/.lash/plugins/yaml-plugin

Built-in plugins are spawned as lash --plugin <name> <socket-path>. External plugins are spawned directly with the socket path as the first argument.

lash --list-plugins sends plugin.describe to each plugin and displays their names, descriptions, hooks, methods, and completions.

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"}
]
}
FieldDescription
methodsMethod names the plugin provides for functional chains.
hooksHook events subscribed to (default mode: blocking).
completionsCommand prefixes the plugin completes (["*"] for all).
hook_config(Optional) Per-event mode and key filter overrides.

Request (backend to plugin):

{"type":"call","method":"csv","args":{},"data":"name,age\nAlice,30"}

Success response:

{"type":"result","data":[{"name":"Alice","age":"30"}]}

Error response:

{"type":"error","problem":"CSV parse failed at row 3","suggestion":"Check for unescaped commas"}

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"]}

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"}]}

Triggered via the plugin.describe hook:

{"type":"hook","event":"plugin.describe","session_id":"","timestamp":"...","data":{}}

Response:

{"action":"modify","data":{"description":"Human-readable plugin description"}}
{"type":"terminate"}

Built-in methods always take priority over plugin methods. Two plugins providing the same method name is a startup error.

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.