openstation

Start Here

Getting started

Install OpenStation and run your first local conversation.

Standing up an OpenStation: scaffold a workspace, talk to the agent, then decide where it should be reachable. Everything below was run against the current code.

Concepts in one breath

  • An OpenStation is a directory with two sibling config planes — .openstation/ (the management overlay) and .claude/ (Claude-native behavior) — plus the workspace files and a git history.
  • An agent is an entry in .openstation/openstation.yaml that references the Claude-native files holding its behavior and permissions.
  • A connector is a channel the agent is reachable on: Slack, Telegram, Email, or a local REPL.
  • The platform is Bun/TypeScript. Your agent's tools can be any language — they run as subprocesses or MCP servers.

Prerequisites

  • Bun — runs the platform.
  • Claude Code CLI, installed and authenticated: either claude login or ANTHROPIC_API_KEY in the environment. OpenStation spawns it; it doesn't bring its own model access.
  • Any tool runtimes your agent needs — a Python venv, say. The platform doesn't need them; the agent does.

The CLI isn't published to npm yet, so run it from a clone:

git clone <this repo> && cd openstation && bun install

Workspaces live in one place — ~/openstation/<name>, one git repo per agent project — so every command below takes a bare name. -d/--dir still points anywhere you like, and with neither the workspace you are standing in — or anywhere under — is used, so the name is optional once you are inside one. See the CLI reference for the precedence rules and OPENSTATION_HOME.

1. Initialize the installation

Once per machine. It creates the home your workspaces live in and a README to catalog them:

bun packages/cli/src/index.ts init
  created ~/openstation
  created ~/openstation/README.md

OpenStation home: ~/openstation
Create your first workspace: openstation create <name>

2. Create a workspace

bun packages/cli/src/index.ts create support
  created .openstation/openstation.yaml
  created .openstation/profiles/member.md
  created .claude/agents/support.md
  created .claude/settings.support.json
  created okf/index.md
  created .env
  workspace trusted in /Users/you/.claude.json — its gate stays in force
  git repository initialized, scaffold committed as 6f2a1c8

OpenStation ready: ~/openstation/support/.openstation/openstation.yaml

The workspace is a git repository from its first second, with the scaffold as commit one — that is what makes every later turn revertible. A .env for this workspace's secrets, plus a .gitignore holding var/, .env, .env.keys, okf/scratch/inbound/, repos/, and .openstation/openstation.local.yaml, are written quietly.

The trust line is the one thing create writes outside the workspace, and it is what keeps the gate below from being ignored: Claude drops an untrusted workspace's whole allow list from every turn, warning on stderr without failing. --no-trust skips it; every boot re-checks it. What the five announced files are for:

.openstation/openstation.yaml — the manifest. It declares the agent and points at the Claude-native files; it never restates them.

# .openstation/openstation.yaml — management overlay; references Claude-native files, never restates them.
agents:
  support:
    identity: agent:support
    executor: claude-cli
    claudeSettings: support
    channels: []
    default: true

claudeSettings names the enforced gate (.claude/settings.support.json); channels: [] is where you'd add a channel this agent answers on; default: true makes it answer anything no other agent claims — dev needs that to boot at all. Minimal on purpose: no budget:, no comments. See "what you can add" below.

.claude/agents/support.md — the charter, read by Claude Code, not by OpenStation:

---
name: support
description: support agent.
---
You are the support agent for this workspace. Answer from what you can read here.

Deliberately no tools: frontmatter. Claude intersects that field with the settings gate, so a stub list here could silently narrow it — the settings file is the sole narrowing.

.claude/settings.support.json — the enforced permission gate:

{
  "permissions": {
    "allow": ["Read", "Glob", "Grep", "Skill", "Edit(okf/**)", "Write(okf/**)"],
    "deny": [
      "Read(.env)", "Read(**/.env)", "Read(.env.keys)", "Read(**/.env.keys)",
      "Read(**/secrets/**)", "Bash(rm:*)",
      "Read(var/**)", "Bash(cat var/*)", "Read(logs/**)", "Bash(cat logs/*)"
    ]
  }
}

Reads everywhere, writes scoped to okf/ — the default writable, git-committed zone — and .env, .env.keys, secrets/, var/, logs/, and rm denied.

.openstation/profiles/member.md — the addendum for the member role:

---
tools: ["Read", "Glob", "Grep", "Skill", "Edit", "Write"]
---
You are talking to a member. Be concise and factual.

tools: here is a ceiling, not a grant — bare verbs, matching the gate's by name. A role with no profile file is refused, so if you plan to use --as admin, write profiles/admin.md first.

okf/index.md — a stub, deliberately without frontmatter (bookkeeping must never match an artifactType: trigger). It makes the bundle exist in git from commit one, since an empty directory doesn't survive a clone. Everything the agent grows underneath — issues/, notes/, whatever it invents — is already watched, committed, and covered by the write grant above; only the bundle root itself is a scaffolded file.

create already made this a repository and committed the scaffold, which is what makes the agent's work reversible — git log in the workspace shows commit one. If create reported that git could not run, fix that now, before the agent writes anything.

What you can add

budget: isn't scaffolded — it's optional, and any default is an opinion. Add one to agents.support in the manifest to cap what a turn may spend:

    budget:
      maxUsd: 0.50    # Claude: native ceiling; Pi: observed post-request threshold
      maxTurns: 8     # claude-sdk and pi-sdk only; claude-cli refuses it

See permissions for what each field does and which executors honor it.

3. Meet the roster (optional)

.openstation/people.yaml isn't scaffolded either — without it, identity is passthrough and every caller gets the member role. Add a person with members add:

bun packages/cli/src/index.ts support members add alice --name Alice --role owner
added member "alice" to ~/openstation/support/.openstation/people.yaml

First use creates the file; a later call appends to it. alice has no channel handle yet — she resolves nothing until you add one by hand. See people.yaml.

4. Talk to it

bun packages/cli/src/index.ts support dev
openstation dev — agent "support"
openstation repl — Ctrl-D to exit, /new to reset the session
you>

Type a message and you get a reply. /new starts a fresh session; Ctrl-D exits. No credentials beyond Claude auth, no daemon.

The scaffolded agent can read anything and write only under okf/ — ask it about the workspace, or have it file something there. To reach further — other paths, Bash, a custom tool — that's your first agent.

5. Add another agent

bun packages/cli/src/index.ts support agents add reviewer --channel review
added agent "reviewer" to ~/openstation/support/.openstation/openstation.yaml

Config only. It appends a manifest entry and writes the .claude/agents/reviewer.md and .claude/settings.reviewer.json that entry references.

One thing to know now rather than later: channels: does not route messages yet. Agent selection resolves once at startup, so one process serves one agent. Two agents means two processes with different -a values. See internal/roadmap.md under "Known gaps to v1".

A whole pipeline can be scaffolded the same way: support loops add copies in the eval → learn → improve loop — two trigger-fired agents that apply approved improvements, their charters, gates and stores, and the rules that fire them — out of a template bundle the CLI ships. It is content you then own and have to finish (each charter has a ## Workspace knowledge section to fill in); see loops.

6. See what happened

bun packages/cli/src/index.ts support events -n 20

On a fresh workspace:

no events recorded yet

That's expected: chat turns publish no events. The log records commits, triggers, scheduled runs, and approvals — not conversations. Once an agent with write access changes something, you'll see its commit:

2026-07-25T07:00:02.256Z  workspace.CommitMade     sha=1ff8221… filesChanged=2 message=agent: workspace update

See the events reference. For now rather than history — is the daemon up, what's in flight, what's queued — there's status.

7. Put it on a real channel

By default connectors start from credentials in the environment — there's no flag:

export SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-...
bun packages/cli/src/index.ts support serve

With no credentials present, serve tells you so and exits. Per-connector setup: Slack · Telegram · Email.

To name your connectors instead — two bots of one type, credentials by reference — declare a connections: block. For the whole configuration in one pass, see end to end; for running it as a long-lived service, deploying.

Secrets

Never in openstation.yaml, never in plaintext in the repo. Supply them by environment — a systemd EnvironmentFile, container env, or your shell — and keep the agent's settings denying Read(.env), as the scaffold does. A workspace can also declare where its own variables come from, with an env: block: a .env by default, or one AWS Secrets Manager secret — or commit an encrypted .env with the dotenvx provider, so the secrets travel with the repo and only the private key stays out; see sharing secrets with dotenvx. See environment variables.

Managing more than one

Every command above named its workspace first — openstation support dev, not openstation dev support. Three commands work on the set instead of one member of it:

bun packages/cli/src/index.ts list              # what is here, and whether each would boot
bun packages/cli/src/index.ts support disable   # stop serving it, keep it
bun packages/cli/src/index.ts support enable    # serve it again

list scans ~/openstation for directories holding .openstation/openstation.yaml, so nothing has to enrol a workspace for it to appear. A disabled workspace is refused by serve and merely warned about by dev — the local loop is where you debug one you have taken off its channels.

Next

View Markdown source on GitHub ↗