openstation

Core Concepts

Workspaces and Git

Understand artifact storage, commits, and workspace boundaries.

The workspace is the directory the agent works in. It's an ordinary git repository, which is what makes the agent's work reviewable and reversible instead of something that happened in a chat window.

Four kinds of state, kept apart

What Where Committed?
the agent's work workspace files (the okf/ bundle — issues/, notes/, scratch/, index.md, and whatever else you grow under it) yes
its history git commits it is the history
platform state var/ — the sessions DB, the event log never
secrets the environment, filled by the provider the manifest's env: block names — by default a gitignored .env at the root never in plaintext — a dotenvx workspace commits an encrypted .env, and its private key never is

The separation is the design. Customer work belongs in git where it can be diffed and reverted; platform bookkeeping belongs in var/ where it can be deleted without losing work; secrets reach the agent through an environment resolved for this workspace, never through a tracked plaintext file. What fills that environment is itself declared — an env: block naming one provider: a .env by default, a committed encrypted .env (dotenvx), or one AWS Secrets Manager secret — and the result is a value handed to the Claude child, not a write into the platform's own environment. See environment variables, including what a .env does not protect.

The scaffold writes a .gitignore containing var/, .env, .env.keys, okf/scratch/inbound/, repos/ (an external checkout, not this workspace's content), and .openstation/openstation.local.yaml (one machine's local override of the committed manifest — committing it would deploy a local run's decisions), and commit automation refuses to stage the platform's own planes (var/, logs/) even when they aren't ignored — so the event log written during a turn can't end up in that turn's commit.

The artifact zone

Two separate boundaries are at work, and it's worth keeping them apart.

What the agent may write is decided by its settings file. In the notes-agent example:

"allow": ["Edit(okf/**)", "Write(okf/**)", "..."]

The grant covers the whole okf/ bundle at once — issues/, notes/, scratch/, or a folder the agent invents underneath, all of it is already writable. Growing the bundle costs no allow-list edit; only widening past okf/ itself does.

What gets committed is what git reports as changed, minus the platform's own planes:

Top-level entry Plane Committed?
var/, logs/ platform state never, ignored or not
anything in .gitignore no
everything else git reports yes

So okf/issues/, okf/index.md, a stray report.md, an edit to CLAUDE.md, a new .claude/settings.support.json — all committed. .gitignore is the knob: it is the workspace's own declaration of what it keeps, and var//logs/ are subtracted on top of it because the control-plane DB must stay out of a commit even if that file is wrong.

This replaced an allow-list of four top-level names, which silently dropped everything else. An agent asked to update its own charter had the edit vanish with no error and no event — the failure mode that made "visible and revertable" the better default. The cost is the other direction: a file the agent writes anywhere it can write will be committed, so okf/scratch/inbound/ — where connectors materialize inbound photos and uploads — is gitignored to keep binaries out of history.

A workspace scaffolded before this collapse has issues/, notes/, scratch/, and index.md at the root instead of nested under okf/. Move them in one step, inside the workspace's own repo:

mkdir okf && git mv issues notes scratch index.md okf/

Then two edits, or the agent can read but not write its own store. Update any triggers: path: filters in .openstation/openstation.yaml to the new okf/... prefix, and replace the old path-scoped grants in each .claude/settings.<agent>.json — entries like Edit(issues/**) or Edit(index.md) name paths that no longer exist — with the pair today's scaffold writes: Edit(okf/**) and Write(okf/**). Serving the workspace backfills the new .gitignore entry (okf/scratch/inbound/) either way; the stale scratch/inbound/ line is harmless and safe to delete by hand.

What gets watched

Watching and writing are different axes, and only one of them auto-extends to a folder you invent. The file watcher walks okf/ — the whole bundle, recursively — so a folder created anywhere underneath it, however deep, is watched with no declaration: drop a file with type: Playbook frontmatter under a brand new okf/playbooks/ and a trigger naming that artifactType: fires, the same day you invented the folder.

A top-level folder of your own devising — loops/ at the workspace root, say — is a different story. Nothing stops you creating it, and the commit path stages it like anything else the agent can reach, but the watcher never walks it: watching is scoped to the single declared root, and there's no config knob to add a second one. A folder meant to be watched — for triggers, automations, anything event-driven — belongs under okf/.

Turns become commits

A turn that changes anything git tracks produces a commit. Both dev and serve do this, so local sessions are as reversible as served ones.

From a real run of the notes example — a single message asking it to record a decision:

$ git log --oneline
1ff8221 agent: workspace update
7b34214 seed

$ git show --stat HEAD
 okf/index.md                               |  1 +
 okf/issues/interval-schedules-over-cron.md | 28 ++++++++++++++++++++++++++++
 2 files changed, 29 insertions(+)

The event log was written to var/events/ during that same turn and is absent from the commit.

Undo is exactly what you'd expect:

git revert HEAD

Commits you can see, and commits that didn't happen

Each commit publishes a workspace.CommitMade event:

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

A commit refused by validation publishes workspace.CommitVetoed with a reason instead. That's deliberate: the change that was blocked is usually the one you want to know about, and a silent veto would be invisible. See the events reference.

Where workspaces live

One directory holds them all — ~/openstation/ by default, overridable with OPENSTATION_HOME — with one git repo per agent project:

~/openstation/
├── README.md        ← what each workspace is for; you maintain it, nothing reads it
├── notes/           ← a workspace, its own repo
└── support-bot/     ← a workspace, its own repo

This matches the runtime topology: one serve process per OpenStation, each with its own workspace, connectors, and concurrency pool (deploying).

The home directory is deliberately not a git repo, and shouldn't become one. Commit automation runs git with cwd set to the workspace root; if a workspace is missing git init, git resolves upward, and the agent's commits land in the parent's history instead of failing. A non-repo parent turns that silent misfile into an error.

For the same reason, don't put an OpenStation at the home directory itself. The commit classifier keys off top-level names, and okf is the artifact zone — so a workspace you happened to call okf/ would be staged into the parent's commits, while its siblings were silently skipped. The footgun used to require matching one of four names; collapsing the zone to one entry shrinks the odds, but doesn't retire the rule.

Setting one up

create makes the repository for you — git init plus a first commit holding the scaffold:

bun packages/cli/src/index.ts create my-space

Pointed at a directory that is already a repo, it scaffolds into it and leaves that history alone — it commits only into a repository it created:

bun packages/cli/src/index.ts create -d ~/code/my-repo -a assistant

Either way, check what it reported. If git could not run — not installed, or no committer identity configured — the scaffold still lands but the repository doesn't, and without one you still get replies while the agent's work accumulates untracked.

Remotes

Nothing pushes for you. If you want the workspace backed up or reviewable elsewhere, add a remote and push it like any other repo — the workspace has no special git requirements beyond being a repository.

View Markdown source on GitHub ↗