How commands run
What a plugin command sees when it runs, and what BeSwarm does with it.
How a command runs
- Working directory: the plugin root (the directory holding
beswarm.toml). Relative paths like["sh", "save.sh"]resolve from there. - No shell: commands are exec'd directly. For pipes, globs or
$VARS, start a shell yourself:["sh", "-c", "echo $BESWARM_SESSION | tee out.txt"]. - stdin carries the context as JSON, then closes. A command that never reads it is unaffected.
- Non-blocking:
plugin runreturns as soon as the command starts. It gives you a run id, not the result; check the outcome withplugin history. - Timeout: each command runs in its own process group. When its
timeoutelapses (default 30s), the whole group gets SIGTERM, then SIGKILL 2 seconds later if it's still alive. The run endstimed_out. - End of a run: a run ends when the command itself exits. A background child that keeps the output pipe open doesn't hold the run open.
- Output: BeSwarm keeps the last 16 KiB of stdout and of stderr per run. When more was written, the text starts with
[… earlier output dropped]. - Concurrency: a plugin runs at most 4 commands at once. A run past that is refused, not queued:
plugin runfails withplugin_command_limit_reachedand the history records it asrefused.
Run status
| Status | Meaning |
|---|---|
running | Still going. |
ok | Exited with code 0. |
failed | Non-zero exit, killed by a signal, or couldn't start. |
timed_out | Stopped at its timeout. |
refused | Not started: the plugin already had 4 runs going. |
The TUI keeps the last 100 runs, across all plugins, in memory. Read them with beswarm plugin history.
Context
Every run gets one JSON document. Commands read it on stdin; a view's stdin is its terminal, so it gets the path of a file holding it in BESWARM_CONTEXT_FILE.
{
"v": 1,
"plugin": "example.notes",
"run": { "kind": "hook", "name": "save", "from": "hook" },
"session": "default",
"dirs": {
"plugin": "/Users/me/code/notes-plugin",
"data": "/Users/me/.beswarm/plugins/data/example.notes-1a2b3c4d"
},
"tab": { "id": "18d47dc212160d20-1", "title": "api", "cwd": "/Users/me/code/api", "workspace": "api" },
"agent": { "kind": "claude", "name": "reviewer", "status": "done" },
"event": { "name": "agent.done", "data": { "from": "working" } }
}| Field | Meaning |
|---|---|
v | Schema version, 1. Fields may be added without bumping it; removing or changing one bumps it. |
plugin | Your plugin's id. |
run.kind | command, hook or view. |
run.name | The command or view name. For a hook, the command it runs. |
run.from | panel (the Plugins panel), menu (a workspace's right-click menu), api (plugin run / plugin view, or any other control-socket caller) or hook. |
session | The session name. |
dirs.plugin | The plugin's directory. |
dirs.data | The plugin's data directory. |
tab | The tab the run is about. For an event, the tab the event concerns; for a panel or CLI run, the tab you ran it from, else the focused one. cwd and workspace are left out when unknown. Omitted when there's no tab. |
workspace | Runs from a workspace's right-click menu only: the workspace's name and path. |
agent | The agent in tab: kind, name (its agent name, left out when unset) and status. Omitted when no agent is detected. |
event | Hooks only: the event's name and its data (see Events). |
Parse it with jq, Python or similar:
ctx=$(cat)
status=$(printf '%s' "$ctx" | jq -r '.agent.status // "none"')Environment variables
Only a few are added to the environment you started BeSwarm with:
| Variable | Meaning |
|---|---|
BESWARM_BIN | The running beswarm binary. Use it to call back into BeSwarm. |
BESWARM_SESSION | The session name. |
BESWARM_TERMINAL | The id of the context tab; unset when there is none. A view keeps its own tab's id, so commands run from a view act as that tab. |
BESWARM_CONTEXT_FILE | Views only: the path of the context file, <data dir>/view-<name>.json, rewritten each time the view opens. |
Data directory
Each plugin gets one directory, ~/.beswarm/plugins/data/<slug>/, created before each run. Keep everything your plugin writes there — settings, state, logs, caches — never in the plugin root, which someone else might own or keep under git. BeSwarm never reads, cleans or deletes it. Read the path from dirs.data rather than building it yourself.