bonsai.yaml, this project's Bonsai settings: what each field means, its allowed values and the template bonsai init writes. Use it to read or change a setting: a pack's ref or its roles' limits, where documents live, the protected paths, the ladder, how long generated files are kept.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
SSkills Directory ProSkills Directory
Get any skill into Claude in one click.
Are you the author of Workspace?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/laststep-workspace)
---
name: workspace
description: "bonsai.yaml, this project's Bonsai settings: what each field means, its allowed values and the template bonsai init writes. Use it to read or change a setting: a pack's ref or its roles' limits, where documents live, the protected paths, the ladder, how long generated files are kept."
---
<!-- Generated by go generate from Bonsai's code (internal/reference/gen); never edit it by hand: a test rebuilds it and fails on any difference. -->
# bonsai.yaml: this project's Bonsai settings
**What it is.** `bonsai.yaml`, at the project's top folder, holds the project's Bonsai settings in the format `bonsai.workspace/1`: the packs it links and their refs, where it keeps the documents Bonsai knows, the paths agents change only under a grant, the ladder that proves work done, how long generated files are kept, and the limits of its packs' roles. It is the one Bonsai file a person edits. `bonsai init` writes it from the template below, a comment on every key; a writer keeps the comments, and `bonsai check` does not require them.
**When to use this skill.** Before reading or changing a setting: what a field means, the values it takes, and what changing it does. Every field and every allowed value is also printed by `bonsai check --schema bonsai.workspace`.
**How a change takes effect.** The file is committed and person-only: an agent changes it only while its running task's `bonsai.allows` holds it, which a person grants. A pack's ref, a pack added or taken out, `never_edit`, `never_read` and the folders under `documents` take effect at `bonsai update` (it previews every change; `--yes` writes them). The ladder, its floor, the ratchets and `ci_marked_tests` count as committed at the main checkout's HEAD, and one that sets a check looser (a rung or a floor entry taken out or changed, a rung added, a ratchet lowered) waits for a person's `bonsai update` to record it. A role's limits under `packs[].roles` and the `messages` thresholds change any time: raising a default or a max is never a loosening.
**What `never_read` stops, from Claude Code's permissions docs.** Each path becomes the deny rule `Read(/<path>)`, anchored at the project's top folder. It also denies the Edit and Write tools on the path (not NotebookEdit: list the path in `never_edit` as well for that). Claude Code applies it best effort to Grep, Glob, `@` mentions and the shell reads it recognises (`cat`, `head`, `tail`, `sed` and others); it does not stop a command that reads without naming the file (`grep -r` from its folder) or a script that opens the file itself. For shell reads, Claude Code's sandbox (`sandbox.filesystem.denyRead` in its settings) blocks every process: it is off by default, and native Windows has none. Bonsai's guard does not judge reads.
## Fields
| Field | Meaning | Allowed values | Example |
|---|---|---|---|
| `format` | The format and its version; a newer one is refused, never guessed | `bonsai.workspace/1` | `"bonsai.workspace/1"` |
| `id` | This project's id, written once by bonsai init; a copy meant as a new project: bonsai init --new-id | text matching `^ws-[a-z0-9]{26}$` | `"ws-7kq2m4xw5r3t6y2u7p4a5c3e2b"` |
| `name` | A short name: a lower-case letter, then up to 39 lower-case letters, digits and dashes | text matching `^[a-z][a-z0-9-]{0,39}$` | `"example"` |
| `packs` | The packs this project uses, applied in this order (a path two packs write stops update) | a list of mappings (their fields below) | as in the template |
| `packs[].id` | The pack's id, as its bonsai/pack.yaml says | text matching `^[a-z][a-z0-9-]*$` | `"base"` |
| `packs[].source` | The git repository the pack comes from, fetched with this machine's git | text matching `^[^-]` | `"https://example.com/packs/workflow.git"` |
| `packs[].path` | The pack's folder inside that repository; null for the repository's top | text matching `^[^/\\:][^\\:]*$`, or null | `"packs/base"` |
| `packs[].ref` | A release tag (base-v1.0.0 for base) or a 40-character commit; change it, then run bonsai update | text matching `^[^-]` | `"base-v1.0.0"` |
| `packs[].roles` | This project's limits for the pack's roles; null keeps the pack's; changed any time | a mapping, or null | under Mappings, below |
| `documents` | Where this project keeps the documents Bonsai knows: a folder (one file each) or one file | a mapping (its fields below) | as in the template |
| `documents.task` | The tasks' folder: one file per task, named <id>-<slug>.md (bonsai.task/1) | text matching `^[^/\\:][^\\:]*$` | `"work/tasks"` |
| `documents.run` | The run reports' folder: one report per agent session (bonsai.run/1) | text matching `^[^/\\:][^\\:]*$` | `"work/runs"` |
| `documents.answers` | The file for answers to questions that name no file of their own | text matching `^[^/\\:][^\\:]*$` | `"work/answers.md"` |
| `documents.memory` | The memory notes' folder, with their INDEX.md, which the block in CLAUDE.md imports | text matching `^[^/\\:][^\\:]*$` | `"work/memory"` |
| `documents.protocols` | The folder the packs' always-on protocol files go to; the block in CLAUDE.md imports each | text matching `^[^/\\:][^\\:]*$` | `"work/protocols"` |
| `protected` | Paths (globs) an agent changes only while its running task lists them in bonsai.allows | a list of text matching `^[^/\\:][^\\:]*$` | as in the template |
| `person_only` | Of those, the paths only a person grants (an approve or grant tap) | a list of text matching `^[^/\\:][^\\:]*$` | `[".claude/**","bonsai.yaml",".bonsai/lock.json"]` |
| `never_edit` | Paths no agent changes: deny rules Edit(/<path>), anchored at the project's top folder; e.g. ["ledger.json"] | a list of text matching `^[^/\\:~()][^\\:()]*$` | `["work/ledger.json"]` |
| `ladder_floor` | The rungs every task climbs, whatever its bonsai.ladder lists; e.g. [0, 1] | a list of a whole number from 0 | `[0,1,2,3,4]` |
| `ladder` | The rungs: what each runs and proves (rung, name, kind, command, required, timeout_s and more) | a list of mappings (their fields below) | as in the template |
| `ladder[].rung` | The rung's number; ladder_floor and a task's bonsai.ladder name rungs by it | a whole number from 0 | `0` |
| `ladder[].name` | Its short name, or null | text, or null | `"unit tests"` |
| `ladder[].kind` | Guard (rung 0: the diff against the task's grants), command, or ver-git | `guard`, `command`, `ver-git` | `"command"` |
| `ladder[].command` | The shell command a command rung runs (never bash by name); null for the other kinds | text, or null | `"go test ./..."` |
| `ladder[].required` | True: a red result here skips the rungs after it | true or false | `true` |
| `ladder[].timeout_s` | The longest it may run, in seconds; null for the runner's default | a whole number from 1, or null | `600` |
| `ladder[].ratchet` | The ratchet count it feeds (see ratchets); null for none | any value | `"tests"` |
| `ladder[].capture` | The patterns that pick numbers or text out of its output, by name; null for none | any value | under Mappings, below |
| `ladder[].means` | One line: what it proves when green; null for none | text, or null | `"Every unit test passes."` |
| `ladder[].tests` | The form of its test output: TAP, go test -json or JUnit; null when it prints no tests | text, or null | `null` |
| `ladder[].base_setup` | The command run first in a worktree at the merge base (installing dependencies); null for none | text, or null | `"npm ci"` |
| `ratchets` | Counts that may only rise, by name, each the floor a rung's count may not fall below; e.g. tests: 120 | a mapping | under Mappings, below |
| `ci_marked_tests` | Tests allowed to skip in CI only, each by its name in the test output; e.g. ["TestNeedsADisplay"] | a list of text | `["TestNeedsADisplay"]` |
| `generated` | How long generated files are kept, per kind; null keeps all (skill base:generated-files) | a mapping (its fields below) | as in the template |
| `generated.log` | The log, one file per session, in .bonsai/local/log/; default: 30 days after a file's last line | a mapping (its fields below) | under Mappings, below |
| `generated.<kind>.keep_days` | Clean what is older than this many days; null: no age rule | a whole number from 0, or null | `30` |
| `generated.<kind>.keep_newest` | Keep only this many of the newest; null: no count rule | a whole number from 0, or null | `100` |
| `generated.asks` | Questions for a person and their answers, in .bonsai/local/asks/; default: kept | a mapping (its fields below) | under Mappings, below |
| `generated.ladder` | Ladder results by task and commit, in .bonsai/local/ladder/; default: 7 days after the result's finished | a mapping (its fields below) | under Mappings, below |
| `generated.run` | Run reports, committed history, in the run reports' folder (documents.run); default: kept | a mapping (its fields below) | under Mappings, below |
| `generated.sessions` | Rows of the sessions table, in .bonsai/sessions.md; default: kept | a mapping (its fields below) | under Mappings, below |
| `generated.messages` | Messages between agents, in .bonsai/local/messages/; default: 30 days after a thread's last message | a mapping (its fields below) | under Mappings, below |
| `generated.agents` | Each agent's state, in .bonsai/local/agents/; default: 30 days after a session folder's last line | a mapping (its fields below) | under Mappings, below |
| `messages` | When bonsai messages flags a thread; null for Bonsai's defaults (bonsai check --schema bonsai.messages) | a mapping (its fields below), or null | `null` |
| `messages.unanswered_minutes` | Minutes a brief may go with no report once its agent is quiet; null for the default | a whole number from 0, or null | `120` |
| `messages.circling_briefs` | Briefs each way one thread may hold before it is flagged circling; null for the default | a whole number from 0, or null | `3` |
| `never_read` | Paths no agent reads: deny rules Read(/<path>), best effort for shell reads; e.g. [".env"] | a list of text matching `^[^/\\:~()][^\\:()]*$` | `[".env",".env.*","secrets/**"]` |
## The template
What `bonsai init` writes, every field present and a comment on every key; its id, name and pack are the project's own (here placeholders). Its ladder is empty: a project's rungs are its own, since only it knows its test command.
```yaml
# bonsai.yaml: this project's Bonsai settings (format bonsai.workspace/1), the one Bonsai file a person edits.
# bonsai init wrote it from Bonsai's built-in template, a comment on every key. It is committed and person-only:
# an agent changes it only while its running task's bonsai.allows holds it, which a person grants.
# To change a setting, edit its value, then run: bonsai update (it previews every change; --yes writes them).
# Every field, its allowed values and an example: bonsai check --schema bonsai.workspace
format: bonsai.workspace/1 # the format and its version; a newer one is refused, never guessed
# this project's id, written once by bonsai init; a copy meant as a new project: bonsai init --new-id
id: ws-abcdefghijklmnopqrstuvwxyz
# a short name: a lower-case letter, then up to 39 lower-case letters, digits and dashes
name: example
# the packs this project uses, applied in this order (a path two packs write stops update)
packs:
- id: example-pack # the pack's id, as its bonsai/pack.yaml says
# the git repository the pack comes from, fetched with this machine's git
source: "https://example.com/packs/example-pack.git"
path: null # the pack's folder inside that repository; null for the repository's top
# a release tag (base-v1.0.0 for base) or a 40-character commit; change it, then run bonsai update
ref: "v1.0.0"
# this project's limits for the pack's roles; null keeps the pack's; changed any time
roles: null
# where this project keeps the documents Bonsai knows: a folder (one file each) or one file
documents:
task: work/tasks # the tasks' folder: one file per task, named <id>-<slug>.md (bonsai.task/1)
run: work/runs # the run reports' folder: one report per agent session (bonsai.run/1)
answers: work/answers.md # the file for answers to questions that name no file of their own
# the memory notes' folder, with their INDEX.md, which the block in CLAUDE.md imports
memory: work/memory
# the folder the packs' always-on protocol files go to; the block in CLAUDE.md imports each
protocols: work/protocols
# paths (globs) an agent changes only while its running task lists them in bonsai.allows
protected: [".claude/**", "bonsai.yaml", ".bonsai/lock.json"]
# of those, the paths only a person grants (an approve or grant tap)
person_only: [".claude/**", "bonsai.yaml", ".bonsai/lock.json"]
# paths no agent changes: deny rules Edit(/<path>), anchored at the project's top folder; e.g. ["ledger.json"]
never_edit: []
ladder_floor: [] # the rungs every task climbs, whatever its bonsai.ladder lists; e.g. [0, 1]
# the rungs: what each runs and proves (rung, name, kind, command, required, timeout_s and more)
ladder: []
# counts that may only rise, by name, each the floor a rung's count may not fall below; e.g. tests: 120
ratchets: {}
# tests allowed to skip in CI only, each by its name in the test output; e.g. ["TestNeedsADisplay"]
ci_marked_tests: []
# how long generated files are kept, per kind; null keeps all (skill base:generated-files)
generated:
# the log, one file per session, in .bonsai/local/log/; default: 30 days after a file's last line
log:
keep_days: 30 # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
asks: # questions for a person and their answers, in .bonsai/local/asks/; default: kept
keep_days: null # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
# ladder results by task and commit, in .bonsai/local/ladder/; default: 7 days after the result's finished
ladder:
keep_days: 7 # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
# run reports, committed history, in the run reports' folder (documents.run); default: kept
run:
keep_days: null # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
sessions: # rows of the sessions table, in .bonsai/sessions.md; default: kept
keep_days: null # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
# messages between agents, in .bonsai/local/messages/; default: 30 days after a thread's last message
messages:
keep_days: 30 # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
# each agent's state, in .bonsai/local/agents/; default: 30 days after a session folder's last line
agents:
keep_days: 30 # clean what is older than this many days; null: no age rule
keep_newest: null # keep only this many of the newest; null: no count rule
# when bonsai messages flags a thread; null for Bonsai's defaults (bonsai check --schema bonsai.messages)
messages: null
# paths no agent reads: deny rules Read(/<path>), best effort for shell reads; e.g. [".env"]
never_read: []
```
## An example ladder
Two rungs a project might add: the guard rung and its own tests, both on the floor. A rung added is a person's step: the next climb refuses it until a person's `bonsai update` in the main checkout records it.
```yaml
ladder_floor: [0, 1] # every task climbs both rungs
ladder: # the rungs, cheapest first
- rung: 0 # rung 0: the guard rung
name: "guard" # its short name
kind: guard # the branch's changes against the running task's grants
command: null # only a command rung runs a command
required: true # red here skips the rungs after it
timeout_s: 120 # two minutes
ratchet: null # no count to keep from falling
capture: null # nothing picked out of its output
means: "Every change is inside the task's grants." # what green proves
tests: null # it prints no tests
base_setup: null # nothing to set up on the base
- rung: 1 # the project's tests
name: "tests" # its short name
kind: command # a shell command, the same in sh and cmd
command: "go test -json ./..." # the project's own test command
required: true # red here skips the rungs after it
timeout_s: 600 # ten minutes
ratchet: tests # its count, the tests that passed, may not fall below ratchets.tests
capture: null # the count is read from the tests themselves
means: "Every test passes, and no fewer than the floor." # what green proves
tests: go-test-json # its output's form: each test named
base_setup: null # nothing to set up on the base
ratchets: # counts that may only rise
tests: 0 # raised to a green climb's count once there is one
```
## Mappings
The fields whose example is a mapping, written as `bonsai.yaml` holds them: indented `key: value` lines, nested by the field's path. Never write a mapping with content on one line in braces (a flow mapping): Bonsai refuses the file, and in `bonsai.yaml` that stops every call until a person repairs it; `{}` alone is an empty mapping.
```yaml
packs:
- roles:
builder:
minutes:
default: 240
max: 480
ladder:
- capture:
tests: "([0-9]+) passed"
ratchets:
tests: 120
generated:
log:
keep_days: 30
keep_newest: null
asks:
keep_days: null
keep_newest: null
ladder:
keep_days: 7
keep_newest: null
run:
keep_days: null
keep_newest: null
sessions:
keep_days: null
keep_newest: null
messages:
keep_days: 30
keep_newest: null
agents:
keep_days: 30
keep_newest: null
```