Command Reference
Detailed specifications, flag arguments, and JSON schemas for Bonsai commands.
The package is published as @taurgis/bonsai and installs a bonsai binary. One-shot npm execution should use npx @taurgis/bonsai ...; after installation, use bonsai ....
Intentional CLI asymmetries
Bonsai keeps a few asymmetries because they match agent workflows:
- The root URL form (
bonsai <url>) is the primary fetch UX. The underlyingfetchcommand is hidden from the command list, butbonsai help fetchremains the full reference for URL-form flags. status --tierhas no default. When omitted, status evaluates freshness against the cached artifact's own tier; setting a default would silently re-gradestableorvolatileentries.list --artifact-typeomitssectionbecauselistreports page-level artifacts (source,research_note, andindex).prune --artifact-typeincludessectionso agents can clean up every cached artifact type.search --artifact-typefollowslist's rule, sincesearchis page-level too.listandsearchboth default--limitto 10 (max 100), so a broad, unfiltered call never floods an agent's context. A truncated result carriessummary.truncatedandsummary.nextCommand— a ready-to-run command that reproduces the same filters with a raised--limit— so raising it is a deliberate next step, not a silent guess.- Short flags are command-local. For example,
-fmeans fetch--formatbut import--file, and-gmeans tags on cache commands but--globalon config commands. Check each command's help before reusing short flags. - Running
bonsaiwith no arguments at all shows live cache data (the same asbonsai list) instead of root help text, preceded by a two-line identity header (bin: <path>anddescription: <one sentence>) so an agent knows what it is looking at without a separate--helpcall. The header is human-mode only and specific to that true bare invocation — it does not appear on an explicitbonsai list, and--json/--toonoutput stays a clean envelope either way.bonsai help,--help, and-hare unaffected and remain the explicit command reference. setup's default scope is the opposite ofconfig set/config unset's.setup <agent>writes to the project by default (--globalopts into a user-level install), whileconfig set/config unsetwrite to the user-level file by default (--localopts into the project file).setupdefaults to project because a shared hook is the common case for a team repo;configdefaults to user because storage mode is usually a personal preference, not something to commit for everyone.
1. Root fetch command
The primary command. It caches or retrieves a URL by using the URL shorthand for the hidden fetch command.
Usage
bonsai <url> [flags]Positional Arguments
<url>: Required string. The full HTTP or HTTPS URL to crawl.
Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--topic | -t | string | null | Main research topic for metadata (max 200 chars). |
--tags | -g | string | [] | taxonomic tags (can be repeated, max 100 chars each). |
--format | -f | choice | compressed | Output format: compressed or detailed. |
--tier | — | choice | standard | Freshness tier logic: stable, standard, or volatile. |
--ttl | -l | duration | null | TTL duration for freshness (e.g. "2h", "7d", "6m"). |
--max-age | — | duration | null | Maximum cache age to accept (e.g. "2h", "7d", "6m"). |
--force | — | boolean | false | Ignore cached copies and force a full network crawl. |
--dry-run | — | boolean | false | Crawl and extract without writing to cache. |
--allow-stale | — | boolean | false | Suppress exit code 5 when a within-grace stale entry is served after failed revalidation; has no effect once an entry is past its grace window. |
--rendered | — | boolean | false | Force browser-rendered extraction for pages that require client-side JavaScript (e.g. SPA docs). |
--storage | — | choice | (configured) | Override cache location for this run: global or project. Secret-bearing pages are always stored globally. |
--read-only | — | boolean | false | Block filesystem writes/deletes for this invocation (alias --plan). Also honored via BONSAI_READ_ONLY / BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Format command response as machine-readable JSON. |
JSON Output Envelope Schema
{
"schemaVersion": 1,
"command": "bonsai",
"ok": true,
"exitCode": 0,
"stdout": "",
"stderr": "",
"data": {
"schemaVersion": 1,
"command": "bonsai",
"dryRun": false,
"cache": {
"key": "0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7",
"status": "hit" | "miss" | "revalidated" | "refreshed" | "stale" | "would_fetch" | "would_refresh" | "would_revalidate",
"freshness": "fresh" | "stale_grace" | "stale_expired" | "none",
"path": "/path/to/research/cache/0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7.md",
"storage": "global" | "project",
"redirectedToGlobal": false
},
"source": {
"url": "https://example.com",
"normalizedUrl": "https://example.com/",
"captureMethod": "static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source",
"extractionStatus": "extracted" | "agent_supplied" | "failed",
"extractionConfidence": "high" | "medium" | "low",
"qualityNotes": [
"readability extracted main article"
],
"fetchedAt": "2026-06-24T07:33:20.519Z",
"validatedAt": "2026-06-24T07:33:20.519Z",
"staleAfter": "2026-07-24T07:33:20.519Z"
},
"format": "compressed" | "detailed",
"tokenEstimate": 29,
"detailedTokenEstimate": 65,
"content": "Cleaned article text..."
}
}cache.freshness reports the freshness of the entry found at lookup, so it explains why the action was taken (a refreshed result still reports the pre-fetch stale_expired). On a miss it is none: no prior entry existed, so the freshly fetched content has no prior freshness to report.
detailedTokenEstimate is always the true detailed-format size, even when format is compressed — it equals tokenEstimate whenever nothing was actually truncated. See Compression & Token Budgeting.
2. import
Save agent-supplied Markdown text directly to local storage.
Usage
bonsai import [url] [flags]Positional Arguments
[url]: Optional string. The target URL (only for single-source import). Must omit if--source-urlis used.
Command-Line Flags
--stdinor--file <path>: Exactly one input source is required.--file -reads stdin (same as--stdin).--dry-run: Validate the import without writing (also implied by global--read-only/--plan).--source-url: Source URLs for multi-source import (repeatable).--input-format: Input content format (detailedorcompressed). Defaults todetailed.--topic: Main topic (max 200 chars). Required for multi-source import.--tags: Taxonomic tags (repeatable, max 100 chars each).--tier: Freshness tier policy (stable,standard, orvolatile).--ttl: TTL duration for imported note freshness (e.g. "2h", "7d", "6m").--storage: Storage mode (globalorproject). Override the configured cache location for this import. Notes containing secrets are always stored globally and never written to a project cache.--read-only/--plan: Block the write; reportsdryRun: trueandcache.status: "would_import".--toon: Emit the same envelope as--json, encoded as TOON (fewer tokens). Mutually exclusive with--json.--json: Format command response as machine-readable JSON.
Localhost and other private hosts are accepted as cache keys for import. Network fetches to those hosts remain blocked by the SSRF guard. Import stdin and file inputs are capped at 1 MiB. Oversized stdin exits 1 with STDIN_TOO_LARGE; oversized files exit 1 with FILE_TOO_LARGE. A directory passed to --file exits 2 with NOT_A_FILE.
JSON Output envelope data block
{
"dryRun": false,
"cache": {
"key": "sha256-import-hash...",
"status": "imported" | "would_import",
"freshness": "fresh",
"path": "/path/to/cache.md",
"storage": "global" | "project",
"redirectedToGlobal": false
},
"artifactType": "source" | "research_note",
"topic": "React Suspense" | null,
"sourceUrls": ["https://example.com"],
"source": {
"url": "https://example.com" | null,
"normalizedUrl": "https://example.com/" | null,
"captureMethod": "agent_supplied",
"extractionStatus": "agent_supplied",
"extractionConfidence": "high",
"qualityNotes": ["agent-supplied research import"],
"fetchedAt": null,
"validatedAt": "2026-06-24T07:33:20.519Z",
"staleAfter": "2026-07-24T07:33:20.519Z"
},
"format": "detailed",
"tokenEstimate": 145,
"content": "Imported markdown body..."
}For multi-source imports (--source-url repeated, no positional URL), source.url and source.normalizedUrl are null. Use sourceUrls and topic (required for multi-source) to find the note again with list --topic "…".
3. status
Inspect cache state and planning outcomes without performing fetches or writes.
Usage
bonsai status <url> [flags]Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--tier | — | choice | — | Evaluate freshness with this tier; when omitted, use the cached artifact's own tier. |
--ttl | -l | duration | — | TTL duration to evaluate freshness (e.g. "2h", "7d", "6m"). |
--max-age | — | duration | — | Maximum cache age to accept (e.g. "2h", "7d", "6m"). |
--read-only | — | boolean | false | Block filesystem writes/deletes for this invocation (alias --plan). Also honored via BONSAI_READ_ONLY / BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Machine-readable envelope. |
JSON Output envelope data block
{
"cacheKey": "0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7",
"cachePath": "/path/to/0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7.md",
"normalizedUrl": "https://example.com/",
"status": "hit" | "miss" | "stale",
"freshness": "fresh" | "stale_grace" | "stale_expired" | "none",
"action": "would_fetch" | "would_revalidate" | "would_return_cached"
}On a miss, freshness is none: no entry exists, so no freshness applies. stale_grace and stale_expired describe an entry that exists but has aged into the grace window or past it. A miss exits 1 with code CACHE_MISS but still returns data (and an array when multiple URLs are passed). In a multi-URL batch, an invalid or scheme-less later URL becomes a sparse error row ({ status: "error", normalizedUrl, error }, exit 1, code INVALID_URL / MISSING_URL_SCHEME) while prior hit/miss rows stay in data — same keep-prior-hits contract as fetch.
4. inspect
Display cached headers and frontmatter metadata for a URL.
Usage
bonsai inspect <url>Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--read-only | — | boolean | false | Block filesystem writes/deletes for this invocation (alias --plan). Also honored via BONSAI_READ_ONLY / BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Return the machine-readable envelope. |
JSON Output envelope data block
{
"cacheKey": "0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7",
"cachePath": "/path/to/0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7.md",
"normalizedUrl": "https://example.com/",
"status": "hit" | "miss",
"metadata": {
"schema_version": 1,
"artifact_type": "source",
"source_url": "https://example.com",
"source_urls": ["https://example.com"],
"normalized_url": "https://example.com/",
"cache_key": "0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7",
"topic": "example",
"tags": ["test"],
"format_available": ["compressed", "detailed"],
"tier": "standard",
"ttl": null,
"fetched_at": "2026-06-24T07:33:20.519Z",
"validated_at": "2026-06-24T07:33:20.519Z",
"stale_after": "2026-07-24T07:33:20.519Z",
"status": "active"
},
"sections": [
{
"cacheKey": "…",
"anchor": "intro",
"headingPath": "Intro > Overview",
"tokenEstimate": { "compressed": 12, "detailed": 40 }
}
]
}On a miss, metadata is null, sections is [], and the command exits 1 with CACHE_MISS while still returning data (including hit rows in a multi-URL batch). A miss row also carries partOfExistingNote: null for a genuinely uncached URL, or { cacheKey, artifactType, topic, sourceUrls } when the URL has no cache key of its own but is already listed among another cached artifact's source_urls — typically a multi-source research_note imported with --source-url, which keys off topic and content rather than any single URL (see Caching Protocol Specification). The accompanying hint points at bonsai list --url "<url>" to find it, instead of suggesting a fetch that would create an unrelated duplicate entry. Invalid URLs in a multi-URL batch follow the same keep-prior-hits contract as status/fetch (error rows, exit 1).
5. list
Browse the cache by metadata, without printing page content. list filters the whole cache and sorts the matches newest-first, so it answers "what do I have?"
Usage
bonsai list [flags]list takes no positional argument; every filter is a flag, and with no flags it returns the most recent entries across all read roots.
list reports page-level artifacts (source, research_note, and index). The section sub-chunks a page is split into are omitted so a single fetch does not flood the listing — find them with inspect (which lists a page's sections).
Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--topic | -t | string | — | Exact topic (case-insensitive). |
--tags | -g | string | — | Tags to require; all repeated tags must match. |
--freshness | — | choice | — | Freshness state: fresh, stale_grace, or stale_expired. |
--artifact-type | — | choice | — | Artifact type: source, research_note, or index. Section children are omitted from list; use inspect to see them. |
--capture-method | — | choice | — | Capture method: static_fetch, browser_fallback, agent_supplied, route_markdown, or github_source. |
--url | — | glob | — | Source URL glob (case-insensitive, supports *). |
--limit | — | integer | 10 | Cap the result count (1–100). |
--full | — | boolean | false | Return every metadata field (cache key, path, artifact type, tags, capture method, quality notes, timestamps) instead of the minimal default row. |
--read-only | — | boolean | false | Block filesystem writes/deletes for this invocation (alias --plan). Also honored via BONSAI_READ_ONLY / BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Return the machine-readable envelope. |
Results are sorted by validated_at (falling back to fetched_at), newest first, then truncated to --limit. A whitespace-only --topic or --tags value is rejected as INVALID_FLAG_VALUE (same as an empty --url) rather than silently matching everything or nothing — almost always a shell-quoting mistake.
JSON Output envelope data block
By default each row is the minimal shape an agent needs to judge relevance and act next — fetch/inspect the source, gauge freshness, budget tokens:
[
{
"sourceUrls": ["https://example.com"],
"topic": "example",
"freshness": "fresh" | "stale_grace" | "stale_expired",
"tokenEstimate": { "compressed": 29, "detailed": 65 }
}
]Pass --full for every metadata field:
[
{
"cacheKey": "0f115db062b7c0dd030b16878c99dea5c354b49dc37b38eb8846179c7783e9d7",
"path": "/path/to/cache/0f115db0...e9d7.md",
"artifactType": "source" | "research_note",
"sourceUrls": ["https://example.com"],
"topic": "example",
"tags": ["test"],
"freshness": "fresh" | "stale_grace" | "stale_expired",
"captureMethod": "static_fetch" | "browser_fallback" | "agent_supplied" | "route_markdown" | "github_source",
"tokenEstimate": { "compressed": 29, "detailed": 65 },
"qualityNotes": ["readability extracted main article"],
"fetchedAt": "2026-06-24T07:33:20.519Z",
"validatedAt": "2026-06-24T07:33:20.519Z"
}
]The envelope always includes a top-level summary object alongside data — aggregate counts plus an explicit empty-result signal, computed over every matched entry (before --limit):
{
"schemaVersion": 1,
"command": "list",
"ok": true,
"exitCode": 0,
"stdout": "",
"stderr": "",
"data": [ /* at most --limit rows */ ],
"summary": {
"total": 12,
"shown": 2,
"limit": 2,
"truncated": true,
"empty": false,
"byFreshness": { "fresh": 9, "stale_grace": 2, "stale_expired": 1 },
"nextCommand": "bonsai list --limit 12"
}
}empty: true (with total/shown both 0) is the definitive signal for "no matches" — never infer it from an empty data array alone. Under --json, none of this is ever mirrored as a process-stderr tip — the envelope field is the stable agent signal. nextCommand is null unless truncated is true; when set, it reproduces every filter you actually passed with --limit raised to show everything matched (capped at 100), so raising it is copy-paste rather than guesswork.
6. search
Rank cached page-level artifacts by keyword — the content/tag search list doesn't do. --query matches (case-insensitive) against topic, tags, summary, and compressed; every other filter (--topic, --tags, --url, --freshness, --artifact-type, --capture-method) works exactly like list's. search reads only the token-cheap indexed summary/compressed text — never the full detailed body — so ranking a large cache stays fast and never balloons context. Omitting --query behaves like list: every row scores 0 and results sort newest-first instead of by relevance.
Usage
bonsai search --query "<keywords>" [flags]search, like list, reports page-level artifacts only (source, research_note, index) — section children are omitted; use inspect to see them.
Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--query | -q | string | — | Keyword(s) to search across topic, tags, summary, and compressed content. Every term must match somewhere unless --match-any is set. |
--match-any | — | boolean | false | Match any query term instead of requiring all of them (OR instead of AND). |
--topic | -t | string | — | Exact topic (case-insensitive), same matching as list --topic. |
--tags | -g | string | — | Tags to require; all repeated tags must match, same matching as list --tags. |
--freshness | — | choice | — | Freshness state: fresh, stale_grace, or stale_expired. |
--artifact-type | — | choice | — | Artifact type: source, research_note, or index. Section children are omitted from search — use inspect. |
--capture-method | — | choice | — | Capture method: static_fetch, browser_fallback, agent_supplied, route_markdown, or github_source. |
--url | — | glob | — | Source URL glob (case-insensitive, supports *). |
--limit | — | integer | 10 | Cap the result count (1–100), same default as list. |
--full | — | boolean | false | Return every metadata field (as list --full) alongside score/matchedFields/snippet instead of the minimal default row. |
--read-only | — | boolean | false | Block filesystem writes/deletes for this invocation (alias --plan). Also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Return the machine-readable envelope. |
A whitespace-only --query/--topic/--tags/--url value is rejected as INVALID_FLAG_VALUE rather than silently matching everything or nothing — almost always a shell-quoting mistake.
Ranking
Each query term is checked against topic, tags, summary, and compressed; a matched field contributes a fixed score weight (topic scores highest, then tags, then summary, then compressed), with repeated occurrences in summary/compressed capped so one long, repetitive page cannot out-rank a genuine topic/tag match. Results are sorted by score (descending) when --query is given, tie-broken by validated_at then cache key for deterministic output; without --query, results sort newest-first exactly like list.
JSON Output envelope data block
By default each row is the minimal shape an agent needs to judge relevance without a second round trip — the same fields as list's minimal row, plus the score, which fields matched, and a short excerpt around the first content match:
[
{
"sourceUrls": ["https://example.com"],
"topic": "example",
"freshness": "fresh" | "stale_grace" | "stale_expired",
"tokenEstimate": { "compressed": 29, "detailed": 65 },
"score": 112,
"matchedFields": ["topic", "compressed"],
"snippet": "…the surrounding sentence containing the matched keyword…"
}
]matchedFields lists only "topic", "tags", "summary", "compressed" — whichever fields a query term actually matched — in that priority order. snippet is null when --query was omitted, or when a match came only from topic/tags (no content excerpt to show). Pass --full for every metadata field, same as list --full.
The envelope always includes a top-level summary object alongside data, matching list's shape plus one additional flag:
{
"schemaVersion": 1,
"command": "search",
"ok": true,
"exitCode": 0,
"stdout": "",
"stderr": "",
"data": [ /* at most --limit rows */ ],
"summary": {
"total": 12,
"shown": 2,
"limit": 2,
"truncated": true,
"empty": false,
"byFreshness": { "fresh": 9, "stale_grace": 2, "stale_expired": 1 },
"nextCommand": "bonsai search --query cache --limit 12",
"queried": true
}
}queried: false means --query was omitted — every row scored 0 and the sort fell back to list's newest-first order, so a caller can tell "no query was run" apart from "the query matched nothing" (empty: true). nextCommand follows the same contract as list's: null unless truncated is true, otherwise the same invocation (including --query) with --limit raised.
7. prune
Delete cached entries by age, inactivity, or type to reclaim disk space. Pruning spans every read root (project and global), so a key present in both is deleted from both.
Usage
bonsai prune [flags]Two guardrails make accidental deletion hard:
- At least one of
--older-than,--inactive,--artifact-type,--url,--topic, or--tagsis required; runningprunewith no filter exits2rather than matching everything. - The command refuses to delete unless you pass
--yes. Use--dry-runfirst to see exactly what would go.--dry-runand--yesare mutually exclusive — passing both exits2(CONFLICTING_FLAGS) rather than guessing which you meant.
Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--older-than | — | duration | — | Content age threshold (fetched_at, falling back to validated_at), e.g. 30d. Zero durations are rejected. |
--inactive | — | duration | — | Idle time threshold (validated_at, falling back to fetched_at), e.g. 14d. |
--artifact-type | — | choice | — | Artifact type to prune: source, research_note, index, or section. Unlike list, prune includes section children. |
--url | — | glob | — | Source URL glob (case-insensitive, supports *). |
--topic | -t | string | — | Exact topic (case-insensitive), same matching as list --topic. |
--tags | -g | string | — | Tags to require (must match all), same matching as list --tags. |
--dry-run | — | boolean | false | Preview files without deleting. Mutually exclusive with --yes. |
--yes | -y | boolean | false | Confirm deletion. Required for a real prune (rejected under --read-only). |
--read-only / --plan | — | boolean | false | Implicit preview; mutations disabled. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Return the machine-readable envelope. |
--older-than and --inactive are distinct: a recently revalidated but originally-old page can match --older-than while still failing --inactive. When some unlinks fail, prunedCount reports actual deletes and the process exits 1. --topic and --tags reject a whitespace-only value with INVALID_FLAG_VALUE — an empty filter is almost always a shell-quoting mistake, not an intentional "match everything" or "match nothing".
JSON Output envelope data block
{
"dryRun": true,
"prunedCount": 0,
"candidateCount": 3,
"files": [{ "cacheKey": "0f115db0...e9d7", "path": "/path/to/cache/0f115db0...e9d7.md" }]
}On a dry run, prunedCount is 0 and candidateCount reports what a real run would delete. On a real run, prunedCount is the number actually removed.
8. config
Manage where the research cache is stored. Configuration is layered, resolved in precedence order: per-command --storage flag > BONSAI_STORAGE env var > project config (.bonsai.json in cwd) > user config (config.json in the OCLIF config dir) > built-in default (global).
Storage modes
| Mode | Cache location | Read behavior |
|---|---|---|
global (default) | OCLIF data dir (<dataDir>/research/) | Reads the global cache only. |
project | <cwd>/.bonsai/research/ (committable) | Reads the project cache first, then falls back to the global cache. |
The project cache is intended to be shared/committed with a repository. To keep secrets out of version control, any artifact whose content matches a known credential pattern (API keys, tokens, private keys, secret=/token= assignments, etc.) is always written to the global cache, even when project storage is selected. A warning is printed, and the JSON envelope reports redirectedToGlobal: true. The matched secret value is never echoed; only the credential type is named.
Subcommands
# Store this project's research cache inside the repo
bonsai config set storage project --local
# Equivalent inline assignment form
bonsai config set storage=project --local
# Set the user-wide default
bonsai config set storage global
# Inspect values
bonsai config get storage # effective value
bonsai config get storage --local # project file only (shows default + "(not configured)" when unset)
bonsai config list # all keys
# Remove a key (restores the default)
bonsai config unset storage --localJSON shapes
config get --json returns { key, value, configured }. configured is false when nothing beyond the built-in default pins the key: for --global/--local that means the file omits it; for the effective (no-scope) view it means no project file, user file, or valid BONSAI_* env override set it. value is still the usable default/resolved value so agents need no second lookup.
config list --json returns an array of { key, value, configured } entries with the same semantics per key (same array-as-data shape as list).
config set --json returns { key, value, scope, dryRun, status }, where status is "set" or "would_set". config unset --json returns { key, scope, dryRun, status }, where status is "unset" or "would_unset". --dry-run, --read-only/--plan, BONSAI_READ_ONLY, and BONSAI_PLAN_MODE all set dryRun: true and skip the config write.
Flags
--global/-g: target the user-level config file (default forset/unset).--local/--project/-p: target the project-level config file (.bonsai.json).--dry-run: (set/unset) show the change without writing.--read-only/--plan: inherited global flag; (set/unset) preview without writing.--toon: emit the same envelope as--json, encoded as TOON (fewer tokens). Mutually exclusive with--json.--json: machine-readable envelope.
Configuration keys
| Key | Values | Default | Description |
|---|---|---|---|
storage | global, project | global | Where new research artifacts are cached. |
summary | conservative, balanced, aggressive | conservative | How aggressively the compressed variant condenses prose (headings, code blocks, tables, and lists are always preserved). Also settable via BONSAI_SUMMARY. |
9. context
Compact, directory-scoped cache dashboard — total entries, a freshness breakdown, and the most recently touched pages. This is what setup-installed SessionStart hooks pipe into an agent's ambient context; see Ambient session context.
Usage
bonsai context [flags]Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--read-only | — | boolean | false | Skip persisting the search-index sidecar for this invocation (alias --plan). Also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Format command response as machine-readable JSON. |
JSON Output envelope data block
{
"total": 3,
"byFreshness": { "fresh": 2, "stale_grace": 1, "stale_expired": 0 },
"shown": 3,
"entries": [
{ "topic": "Node URL API", "sourceUrls": ["https://nodejs.org/api/url.html"], "freshness": "fresh" }
]
}entries is capped to a small preview (5 by default); total/byFreshness always cover every matched artifact, so the cap never hides the true count. total: 0 is a definitive empty state, not an ambiguous empty list.
10. setup
Installs or repairs a SessionStart hook that runs bonsai context at the start of every agent session. See Ambient session context for the full workflow.
Usage
bonsai setup <agent> [flags]Positional Arguments
<agent>: Required string.claude-codeorcodex.
Command-Line Flags
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--global | -g | boolean | false | Install to the user-level hook file instead of the project (~/.claude/settings.json / ~/.codex/hooks.json). |
--local | — | boolean | (default) | Explicit form of the project-scoped default (.claude/settings.json / .codex/hooks.json under cwd). Mutually exclusive with --global. |
--dry-run | — | boolean | false | Preview the install without writing anything. |
--read-only | — | boolean | false | Preview instead of writing (alias --plan). Also honored via BONSAI_READ_ONLY/BONSAI_PLAN_MODE. |
--toon | — | boolean | false | Emit the same envelope as --json, encoded as TOON (fewer tokens). Mutually exclusive with --json. |
--json | — | boolean | false | Format command response as machine-readable JSON. |
JSON Output envelope data block
{
"agent": "claude-code",
"scope": "project",
"path": "/path/to/project/.claude/settings.json",
"binCommand": "bonsai",
"status": "installed" | "repaired" | "unchanged" | "would_install" | "would_repair",
"dryRun": false
}status is "unchanged" when a re-run finds an identical Bonsai-managed entry already in place (idempotent no-op) and "repaired" when one exists but its command is stale (e.g. after a reinstall to a new path) — either way, any other hooks already in the file are left untouched.
opencode is not yet a supported <agent> value — see Ambient session context for why.