Offsider

6of 9

Tools

By default a panel cannot reach the network, so live data comes through tools: commands declared in panel.json.

Approve the commands

Before a panel with tools loads, Offsider shows a native permission window with its folder, working directory, and every tool’s command and resolved executable. Choose Allow All Capabilities or Cancel. There are no per-tool switches in this version. Panels without tools open directly.

Approval covers all declared tools and the JSON arguments the panel sends them. Commands run with your user permissions and terminal credentials, so approve only tools you trust.

Signed, provisioned builds remember approvals in Offsider’s app-scoped macOS Keychain group. Development builds show Allow All for This Session and ask again after Offsider quits. Changing the tool definitions, panel folder, working directory, resolved program, or an executable file requires approval again. Explicit file arguments (such as python3 tools/query.py) are also fingerprinted. The CLI checks these before each call and refuses changed tools until you reopen the panel. Moving a panel or updating a command-line program can therefore trigger another prompt.

Use Forget Panel Approvals… in Offsider’s menu bar to revoke all approvals and close current panels. Running jobs are stopped; ordinary tool calls already in progress may finish.

Run a tool

  • Where they run. In Offsider.app, with the directory and environment captured from offsider open. They continue working after the opening CLI detaches.
  • The command. An array, never passed through a shell. A first element starting ./ is relative to the panel’s folder. Absolute paths run directly; other paths containing / are relative to the opening terminal’s directory. Bare command names are resolved on PATH when opening and pinned to the reviewed executable.
  • Input. offsider.call(name, args) sends args as JSON on stdin. OFFSIDER_PANEL_DIR is set to the panel’s folder.
  • Output. A JSON object on stdout is what call resolves with. Other JSON comes back as { "value": … }, and anything that isn’t JSON comes back as the text.
  • Failures. A non-zero exit, or running past timeout (60 seconds by default), rejects the call with the command’s stderr.
#!/usr/bin/env ruby
# tools/prs: {"numbers": [377, 380]} in, {"prs": [...]} out
require "json"
numbers = JSON.parse($stdin.read).fetch("numbers")
prs = numbers.map { |n| JSON.parse(`gh pr view #{Integer(n)} --json number,state,title`) }
puts JSON.generate(prs:)

For long-running commands, use offsider.run(name, args) instead. It starts a declared tool and returns a job whose events async iterable emits labelled stdout and stderr chunks followed by an exit event. job.cancel() terminates it. Jobs stop when the panel closes. Tools run with your user permissions, so inspect the folder before opening it.

const job = await offsider.run("prepare", { pr: 377 })
for await (const event of job.events) {
  if (event.type === "stdout" || event.type === "stderr") appendLog(event.type, event.text)
  if (event.type === "exit") showExit(event.code)
}

The job receives its arguments as one JSON document on stdin. A job with no declared timeout may keep running until it exits, the panel closes, or the panel calls job.cancel(). Keep job.id if the page may reload; offsider.attachJob(id) replays recent output and then continues with live events. Offsider keeps the most recent 256 events for each of up to 20 jobs per panel.