Launch trusted background commands when OpenCode initializes a project or directory.
This is an unofficial third-party plugin and is not affiliated with the OpenCode project.
- Loads dedicated global and project command files in global-first order without blocking on child readiness.
- Deduplicates launches for each OpenCode process and normalized project root.
- Remembers
stopOnExit: falseprocesses across OpenCode restarts and re-adopts them instead of starting duplicates. - Spawns the executable with its ordered arguments directly, without an implicit shell.
- Supports Linux, macOS, and Windows with sanitized console and rotating file logs.
Supports OpenCode 1.18.x and is compiled and directly tested against 1.18.28.
Register the exact npm release in opencode.json or opencode.jsonc:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-startup-commands@1.2.0"]
}Alternatively, register the immutable Git tag:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-startup-commands@git+https://github.com/PixelWinner/opencode-startup-commands.git#v1.2.0"]
}Plugin options are not used. Commands belong in the files below.
| Scope | Location |
|---|---|
| Global, Linux/macOS | ~/.config/opencode/startup-commands.json |
| Global, Windows | %USERPROFILE%\.config\opencode\startup-commands.json |
| Project | <project-root>/.opencode/startup-commands.json |
Each file is strict JSON; comments and trailing commas are not accepted. A missing file is normal.
{
"commands": [
{
"name": "Start helper",
"executable": "/absolute/path/to/helper",
"args": ["--watch"],
"onExistingProcess": "skip",
"stopOnExit": true
}
]
}commands must be an array. Each valid entry needs a non-empty name, a non-empty executable, and a string array named args. onExistingProcess accepts only start, skip, or restart and defaults to skip; stopOnExit must be a boolean and defaults to true. Invalid entries are skipped without blocking later valid entries.
Project commands receive the original OpenCode worktree root as cwd; only the deduplication key normalizes that root. Global commands inherit OpenCode's current directory. Both inherit its environment and PATH.
- Global commands are loaded before project commands; array order is preserved within each scope.
- One missing or invalid scope does not block valid commands from the other scope.
- Process identity is the exact
executableplus the orderedargs; project identities also include the normalized root, while global identities are shared by OpenCode instances in one process.name,onExistingProcess, andstopOnExitare not part of identity. - Same-batch duplicate removal is global-first and first within each scope: the global/first duplicate winner supplies both policies before policy evaluation.
startappends one additional, separately owned process record.skipattaches the current owner to and reuses the oldest active process record without changing its creation-timestopOnExit.restartstops all plugin-owned tracked records, regardless ofstopOnExit, and starts one replacement only after complete confirmed cleanup.- For
stopOnExit: true, project ownership uses the normalized root and cleanup waits for that project's final owner; global cleanup waits for the final OpenCode plugin-instance owner. stopOnExit: falserecords remain tracked when ownerless for laterskiporrestart;stopOnExit: truerecords wait for the final owner, then stop. A same-process reopen can attach a new owner to a still-tracked record. Confirmed successful final cleanup instead releases the identity, so reopening in the same OpenCode process starts a new process.- Failed launches are not retried in that process; launch failures, final natural exits, and unconfirmed stale cleanup create tombstones and blockers that block same-process retry as applicable.
- A partial restart leaves safely addressable survivors in degraded state, transfers orphaned owners to the oldest survivor, and starts no replacement.
- Tracking covers processes the plugin launched during the current OpenCode process, plus the
stopOnExit: falseprocesses it recorded in its own registry; it does not scan the OS to discover or adopt processes it did not launch. - Bare
opencode servedoes not launch commands until a project or directory initializes the plugin.
A full OpenCode restart is required after updating plugin code or changing registration, revision, or command files; after that restart the plugin cannot rediscover stopOnExit: true processes, which are tracked only in memory, and re-adopts only the stopOnExit: false processes recorded in its registry whose (pid, startToken) pair still matches.
- On POSIX, process-group cleanup sends
SIGTERM, waits 5 seconds, then sendsSIGKILL. - On Windows, the plugin directly invokes trusted
taskkill.exe /Tand waits 5 seconds; only if the tree still remains does it invoketaskkill.exe /T /Ffor forced cleanup. - On Windows, if the tracked root exits before cleanup, its descendants cannot be addressed safely. The plugin fails closed, which may leave descendants running, blocks restart for that identity, and recovery requires a full OpenCode restart.
- Cleanup is best-effort for deliberately detached or escaped descendants, forced OpenCode termination, OS crash, and power loss.
A stopOnExit: false process survives an OpenCode exit, and the next OpenCode process re-adopts it from a registry file instead of starting a second copy. A stopOnExit: true process is stopped when its final owner disposes, is never recorded, and is never re-adopted.
The table covers the first activation after a full OpenCode exit, with the command's configuration unchanged since the previous start; within one OpenCode process the policies behave as described above.
onExistingProcess |
stopOnExit |
Behavior at the next OpenCode start |
|---|---|---|
skip |
false |
Reuses the surviving process and starts nothing. |
start |
false |
Keeps the surviving process and starts one more beside it; both are remembered. |
restart |
false |
Stops the surviving process, waits for confirmed cleanup, then starts exactly one replacement. |
skip |
true |
Nothing survived, so one process starts and is stopped again on exit. |
start |
true |
Nothing survived, so one process starts and is stopped again on exit. |
restart |
true |
Nothing survived, so one process starts and is stopped again on exit. |
- Changing
stopOnExitfromfalsetotruetakes effect at the next activation, not the running one: the surviving process is stopped, one replacement starts in per-session mode, and that replacement is stopped when its session closes. - Removing a command from a cleanly loaded configuration stops its durable process. The record is kept unless that stop is confirmed, so a process that cannot be confirmed stopped is retried on every later cleanly loaded activation, and each retry waits out the stop budget before other durable work proceeds. A configuration error stops nothing: adoption still runs, because refusing to adopt over a typo would create the duplicate processes this section exists to prevent, while stopping is destructive and requires both the global and the project file to load with no error at all, including a single invalid entry.
- If the plugin cannot determine whether a remembered process is alive, such as when a probe is denied permission, it fails closed: it neither stops that process nor starts a duplicate, and retries at a later activation.
- No external runtime, daemon, or supervisor process is added, and the plugin declares no runtime dependencies.
A registry file records, for each surviving process, a hash of the command identity, its PID, and a start token:
| Platform | Registry file |
|---|---|
| Windows | %LOCALAPPDATA%\opencode\startup-commands\<hostname>\registry.json |
| macOS | ~/Library/Application Support/OpenCode/startup-commands/<hostname>/registry.json |
| Linux | $XDG_STATE_HOME/opencode/startup-commands/<hostname>/registry.json, or ~/.local/state/opencode/startup-commands/<hostname>/registry.json |
The registry is kept per host, under a sanitized <hostname> directory, so a home directory shared between machines keeps each machine's processes separate. Because that key is the hostname, renaming the machine starts a new registry: helpers recorded under the old name are forgotten, never stopped, and the next start launches fresh ones beside them, so stop the old ones once by hand after a rename. A remembered process is re-identified by the (pid, startToken) pair, never by the token alone. A start token is only a start timestamp at the platform's own granularity: 100-nanosecond ticks on Windows, one kernel clock tick on Linux (10 ms on the kernels measured), and one second on macOS. No token contains a PID, so two processes started inside one such window can carry the same token; that is safe because every comparison the plugin makes is keyed by PID as well.
A deleted registry file, or a record whose (pid, startToken) pair no longer matches, for example after a reboot, means the process it described is forgotten rather than stopped, and the command starts fresh. A registry file that cannot be parsed is set aside under a .corrupt- name and the plugin continues with an empty registry, so the processes it described are likewise forgotten rather than stopped. A registry file that cannot be read at all is left in place instead, and stopOnExit: false commands are skipped for that activation rather than started. Executable paths, arguments, environment variables, and project paths are never recorded.
Version 1.1.0 kept no record of the processes it left running, so 1.2.0 cannot identify them. Once, before upgrading:
- Stop any helpers left running by
stopOnExit: false. - Fully exit every OpenCode process.
- Install 1.2.0.
- Start OpenCode. New
stopOnExit: falsecommands are now remembered.
Warning
This plugin performs arbitrary code execution with the OpenCode user's permissions. Install only from a plugin source and command configuration you trust, and inspect project-controlled command files before opening a repository.
The plugin launches executables directly and does not add shell expansion, interpolation, pipes, redirection, or wildcards. Configure a trusted shell explicitly only when shell behavior is required.
Use absolute executable and script paths when predictable resolution matters. Do not place secrets, credentials, or personal data in names or arguments; local process tools may expose arguments even though plugin logs do not.
Before reporting a problem, sanitize logs, configuration, and other evidence. Follow SECURITY.md for private vulnerability reports.
Events appear as startup-commands: messages on OpenCode's stderr and in:
| Platform | Log file |
|---|---|
| Windows | %LOCALAPPDATA%\opencode\logs\opencode-startup-commands.log |
| macOS | ~/Library/Logs/OpenCode/opencode-startup-commands.log |
| Linux | $XDG_STATE_HOME/opencode/log/opencode-startup-commands.log, or ~/.local/state/opencode/log/opencode-startup-commands.log |
The active file rotates at 1 MiB and retains one .1 archive. Logging failures do not prevent command startup. Logs omit configuration and executable paths, arguments, environment variables, raw configuration, and raw errors; they retain only sanitized lifecycle context such as scope, safe display name, PID, exit code, and signal. Stop logging uses sanitized requested, forced, and failed stop events without process commands or raw errors.
bun install --frozen-lockfile
bun test
bun run typecheck
bun run compile
bun run release:checkdist/ remains tracked for immutable Git installation. After runtime source changes, compile and keep the generated output synchronized with the source.
Released under the MIT License.
Author: Oleksandr Khoroshykh (PixelWinner).