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.