A tool to configure different agentic coding frameworks consistently across installations.
Project description
🚉 Bun Off
Bun Off (short for "bundle off") configures AI coding-assistant
stacks — rules, MCP servers, skills, slash commands, plugins, and
hooks — from a single boff.yaml manifest directly to each platform's
native config locations.
Package: boff (installed as bun-off) | Python: ≥ 3.12 | License: GPL-3.0-or-later
📦 Installation
Bun Off is a command-line tool, so install it into its own isolated environment:
uv tool install bun-off
The boff command becomes available after installation. Check it with:
boff --version
Requirements
- Python ≥ 3.12.
- A
gitbinary onPATH, for manifests that resolve a Git reference and forboff context(which reads the workspace's Git state). Everything else runs without it.
Ready-made bundles
Looking for a stack to deploy right away? The companion repo
bun-off-bundles ships ready-made general-dev
(language-agnostic rules and MCP servers) and python (uv, ruff, pytest, serena) bundles.
Deploy one into the current directory by its URL, with nothing to clone first:
boff deploy https://github.com/atom-sw/bun-off-bundles/tree/main/python --platform claude
🗂️ Manifest layout
A manifest is a folder containing boff.yaml and one subdirectory per artifact type:
my-stack/
boff.yaml
rules/ # one <name>.md per listed rule
skills/ # one <name>.md per listed skill
slash_commands/ # one <name>.md per listed command
agents/ # one <name>.md per listed agent (system-prompt body)
mcp_servers/
raw/
claude/ # <name>.json for each server on Claude
opencode/ # <name>.json for each server on OpenCode
antigravity/ # <name>.json for each server on Antigravity CLI
plugins/ # subtrees referenced by plugin install specs
mise/ # mise.toml files listed under mise:
hooks/ # <name>.py per listed lifecycle hook
event_hooks/ # <name> shell script per listed event hook using script:
Settings (settings:) and event hooks (event_hooks:) are declared inline in boff.yaml;
only event hooks that reference a script: file need the event_hooks/ directory.
boff.yaml reference
meta: # required: documentation describing this manifest
# (name + description are themselves required)
name: my-stack
description: Team Python stack.
long_description: | # optional free-form markdown
Rules, skills, and MCP servers shared across the team's Python services.
version: "1.2.0" # optional
author: Platform Team # optional
homepage: https://example.com/stacks/python # optional
extends: ../base-stack # optional: inherit from one or more parent manifests
# (a single reference, or a list; later parents win, child wins over all)
rules:
- style # short form: name only
- name: lint
category: python-dev # optional: groups rules under a subdirectory
globs: ["**/*.py"] # optional: scope the rule to matching files (Claude only)
available_on: [claude] # optional: restrict to specific platforms
skills:
- format
slash_commands:
- refactor
agents:
- name: reviewer
description: Read-only code reviewer
model: haiku # string: same value on every platform
- name: planner
description: Deep planner
model: # map: a distinct model per platform
claude: opus
opencode: anthropic/claude-opus-4-8
permissions:
allow: [{ tool: read }, { tool: grep }]
deny: [{ tool: edit }, { tool: bash }]
mcp_servers:
- context7
- name: tldr
available_on: [claude]
permissions: # tool-use rules, grouped by verdict
allow:
- { tool: read, pattern: "./src/**" }
ask:
- { tool: bash, pattern: "git push *" }
deny:
- { tool: mcp, pattern: "secret__*", available_on: [claude] }
plugins:
my-plugin:
install:
claude:
source: local
path: plugins/my-plugin # path relative to the manifest root
mise:
- mise/mise.toml
settings: # verbatim per-platform settings, deep-merged natively
claude:
outputStyle: Explanatory
opencode:
theme: tokyonight
event_hooks:
- name: lint
event: after_edit # normalized event: after_edit | after_bash | on_finish
# | session_start | session_end
script: lint # shell script at event_hooks/lint
- name: notify
event: on_finish
command: "notify-send 'done'" # inline shell, no script file
hooks:
pre_install:
- script: pre_hook
post_install:
- script: post_hook
🏷️ Manifest metadata
Every manifest carries a meta: block documenting it or the bundle it
defines. name and description are required, and a manifest missing
either fails to load; long_description, version, author, and
homepage are optional. Metadata is informational: it produces no
deploy operations and is never inherited through extends:. boff deploy
prints the metadata header (and the resolved extends chain) above
its operation summary, so boff deploy --dry-run doubles as a
validate-and-inspect command:
boff deploy ./ai-cannot-code-stack --platform claude --dry-run
# ai-cannot-code-stack v1.2.0
# Python development tools.
# author: The Anti-Automation League
# extends: ../claude-poweruser-stack
# planned 12 operation(s):
# ...
🧬 Extending manifests
A manifest can build on one or more parents with extends:. Each reference resolves to another
manifest folder, which Bun Off loads and merges underneath the current one. extends: accepts a
single reference or an ordered list:
extends: ../base-stack # one parent
extends: # several parents (mixins)
- ../base-stack
- ../python-preset
Reference scheme
A manifest reference names either a local folder or a Git repository. The same scheme applies
wherever Bun Off takes a manifest: an extends: parent, and the <manifest> argument of
boff deploy and boff check.
| Form | Example | Resolves to |
|---|---|---|
| Local path | ../base-stack, /abs/path/stack |
A folder relative to the extending manifest (for a CLI argument: to the current directory), or absolute. |
| Git URL | https://host/org/repo.git/sub/dir@v1.2.0 |
The sub/dir subtree of repo, checked out at ref v1.2.0. |
| Forge URL | https://github.com/org/repo/sub/dir@v1.2.0 |
The same, with the .git left off. |
| Browser URL | https://github.com/org/repo/tree/v1.2.0/sub/dir |
The same, pasted straight from the address bar. |
| Repository root | https://github.com/org/repo |
The boff.yaml at the top of repo, on the default branch. |
Any URL is a Git reference; anything else is a local path. Bun Off finds where the repository URL ends in three steps, taking the first that applies:
- A path segment ending in
.gitends it. This is the only form that works on every host, and the only one that can reach a repository nested below<owner>/<repo>, such as a GitLab subgroup or afile://path. - A browser URL's
treeorblobsegment (GitLab's/-/tree/) ends it, and the segment after it is the Git ref. - Otherwise the first two segments are
<owner>/<repo>, the convention every forge follows.
The /<subdir> and the trailing @<ref> are both optional, and @<ref> defaults to the remote's
default branch. Spell a branch name containing a slash with @<ref>: a browser URL cannot express
one, and an explicit @<ref> overrides the ref a tree segment names.
Bun Off fetches into a cache under $XDG_CACHE_HOME/boff/git/ (or ~/.cache/boff/git/) and
reuses it on later runs. Every spelling of one repository shares a single clone.
Merge semantics
Parents merge in declaration order, then the child overrides all (last definition wins):
| Section | How it merges |
|---|---|
rules, skills, slash_commands, mcp_servers, agents, plugins |
By name: a later definition replaces an earlier one. Bun Off logs a warning for each override. |
event_hooks |
By name, same as above. |
permissions |
Rule lists concatenate; identical rules are de-duplicated. |
settings |
Deep-merged per platform; the child wins on conflicting keys, parent-only keys survive. |
mise and other tool files |
By tool: the inherited and child file lists are concatenated parent-first and de-duplicated by path. |
hooks.pre_install / hooks.post_install |
Concatenated parent-first, de-duplicated. |
meta |
Not inherited: the child's own metadata is kept. |
Inheritance cycles (a manifest that extends itself directly or transitively) raise an error.
⚠️ Resolving a remote reference runs
gitagainst the referenced URL, and deploying a manifest runs itspre_installandpost_installhooks. Only extend and deploy manifests you trust.
📄 Artifact types
Rules
Rules are markdown files that provide persistent instructions to the AI assistant. Each rule
name resolves to rules/<name>.md in the manifest folder.
The optional category: field places the rule file under a named subdirectory on disk:
| Platform | Path without category | Path with category: python-dev |
|---|---|---|
| Claude Code | .claude/rules/<name>.md |
.claude/rules/python-dev/<name>.md |
| OpenCode | .opencode/rules/<name>.md |
.opencode/rules/python-dev/<name>.md |
| Antigravity CLI | all rules inlined into GEMINI.md |
category: is ignored |
OpenCode also receives an instructions glob entry in opencode.json that makes it load all
rules from .opencode/rules/**/*.md.
Antigravity CLI reads rules only from its primary instructions file: it never loads
.agents/rules/*.md, and it does not expand @-includes. Bun Off therefore concatenates every
rule into a generated GEMINI.md, one ## <name> section per rule, in manifest order. It writes
GEMINI.md rather than AGENTS.md for two reasons: AGENTS.md is also OpenCode's primary
instructions file, so deploying both platforms into one workspace would inject every rule twice,
and AGENTS.md is commonly hand-authored. Antigravity loads both and merges them, so your own
AGENTS.md keeps working alongside the generated GEMINI.md.
boff deploy never writes AGENTS.md. boff context does: it treats AGENTS.md
as Antigravity's instructions file, exactly as it treats CLAUDE.md on Claude. Splitting the two
files this way means a later boff deploy regenerates GEMINI.md without discarding migrated
context.
The optional globs: field scopes a rule so it applies only when the assistant works on matching
files, instead of loading unconditionally. It takes a list of glob patterns:
rules:
- name: csharp-style
category: dotnet
globs:
- "**/*.cs"
- "**/Controllers/**"
Platform support differs:
| Platform | Behavior |
|---|---|
| Claude Code | Bun Off prepends a globs: frontmatter block to the rule file (the key Claude Code honors: the documented paths: key is silently broken). The rule loads only when matching files are in play. |
| OpenCode | OpenCode has no conditional, path-scoped loading: its instructions globs only select which files to always load. Bun Off deploys the rule unscoped (as it does today) and logs a warning that the scope is not enforced. |
| Antigravity CLI | Rules are inlined into GEMINI.md, which loads wholesale. Bun Off deploys the rule unscoped and logs a warning that the scope is not enforced. |
Two caveats:
- Claude Code honors
globs:only for workspace-level rules (.claude/rules/), not user-level rules. Indeed, Bun Off deploys to the workspace. globs:patterns are written verbatim into a double-quoted YAML string, so a pattern must not contain a double-quote character.
Skills
Skills are markdown files that teach the assistant a repeatable workflow. Each name resolves to
skills/<name>.md.
| Platform | Path |
|---|---|
| Claude Code | .claude/skills/<name>/SKILL.md |
| OpenCode | .opencode/skills/<name>/SKILL.md |
| Antigravity CLI | .agents/skills/<name>/SKILL.md |
Antigravity requires name and description frontmatter in every SKILL.md: it reads the
description to decide whether to activate the skill.
Slash commands
Slash commands are markdown files that define custom /commands. Each name resolves to
slash_commands/<name>.md.
| Platform | Path |
|---|---|
| Claude Code | .claude/commands/<name>.md |
| OpenCode | .opencode/commands/<name>.md |
| Antigravity CLI | not supported: warns and skips |
Antigravity CLI's slash commands are built in, and it discovers no author-supplied command
directory in the workspace. Scope your commands with available_on: [claude, opencode] to
silence the warning.
MCP servers
MCP server entries inject per-platform verbatim JSON config into the platform's settings.
Each name requires a JSON file at mcp_servers/raw/<platform>/<name>.json. The content is the
raw server object exactly as the platform expects it (transport, command, args, env, etc.).
| Platform | Target file | Merge behaviour |
|---|---|---|
| Claude Code | .mcp.json |
Deep-merged into mcpServers |
| OpenCode | opencode.json |
Deep-merged into mcp |
| Antigravity CLI | .agents/mcp_config.json |
Deep-merged into mcpServers |
Antigravity names a remote server's endpoint serverUrl: the legacy url and httpUrl keys
are not read.
Because MCP server config is platform-specific, you must supply a separate JSON file for each platform the server targets.
Permissions
The permissions: block states which tools the assistant may use, in platform-neutral terms.
Bun Off translates each rule into the target platform's native permission syntax. Rules are grouped
under three verdicts: allow, ask, and deny.
permissions:
allow:
- { tool: read, pattern: "./src/**" }
- { tool: webfetch, pattern: "github.com" }
ask:
- { tool: bash, pattern: "git push *" }
- { tool: edit }
deny:
- { tool: bash, pattern: "curl *" }
- { tool: mcp, pattern: "secret__*", available_on: [claude] }
Each rule needs a tool. The optional pattern narrows the rule to matching invocations, and
the optional available_on restricts it to specific platforms, exactly as on any other artifact.
A rule with no pattern covers every use of the tool.
Tool names are canonical, not platform-native. Bun Off accepts these fifteen:
bash, read, edit, write, glob, grep, webfetch, websearch, agent, mcp, lsp,
skill, question, external_directory, doom_loop
| Platform | Target file | Merged under |
|---|---|---|
| Claude Code | .claude/settings.json |
permissions, split into allow / ask / deny lists |
| OpenCode | opencode.json |
permission, keyed by tool |
| Antigravity CLI | not supported: warns and skips |
Antigravity keeps its permission allowlist in the machine-global
~/.gemini/antigravity-cli/settings.json, which a workspace-scoped deploy must not write.
Not every tool exists on every platform, and a rule naming a tool the target cannot express is
an error, not a warning. Scope such rules with available_on:. Claude rejects lsp, skill,
question, external_directory, and doom_loop; OpenCode rejects mcp. The error message names
the platform to scope the rule to.
Translation is otherwise mechanical, with three cases worth knowing:
agentbecomesTaskon Claude andtaskon OpenCode.webfetchwith a pattern becomesWebFetch(domain:<pattern>)on Claude, so write the pattern as a bare domain.mcpbecomesmcp__<pattern>on Claude, or plainmcpwhen the rule has no pattern.writecollapses intoediton OpenCode, which does not distinguish the two.
Where a tool carries several rules, Bun Off writes them to OpenCode ordered allow, then ask, then
deny. OpenCode applies the last match, so this ordering reproduces Claude's precedence, in which
a deny outranks an ask and an ask outranks an allow.
Per-agent permissions use this same rule shape inside an agent definition, and are scoped to that subagent rather than the session.
Settings
The settings: key carries a verbatim per-platform settings block that Bun Off deep-merges into
each platform's native settings file. Use it for any project-shareable setting Bun Off does not
model with a dedicated artifact: Claude's outputStyle, env, cleanupPeriodDays; OpenCode's
theme, formatter, autoupdate, and more.
settings:
claude:
outputStyle: Explanatory
cleanupPeriodDays: 14
opencode:
theme: tokyonight
| Platform | Target file | Merge behaviour |
|---|---|---|
| Claude Code | .claude/settings.json |
Deep-merged at the top level |
| OpenCode | opencode.json |
Deep-merged at the top level |
| Antigravity CLI | not supported: warns and skips |
Antigravity CLI keeps its settings, including its permission allowlist, in the machine-global
~/.gemini/antigravity-cli/settings.json. It has no workspace settings file, and a
workspace-scoped deploy must not write a global one: two projects would clobber each other.
A settings block is a raw passthrough: Bun Off does not validate the values. To keep the modeled concepts
canonical, Bun Off rejects keys that a dedicated artifact already owns: permissions and
mcpServers on Claude; permission, mcp, and instructions on OpenCode. Configure those
through permissions:, mcp_servers:, and rules:
instead.
Agents
An agent is a focused subagent: a system-prompt body plus per-agent metadata. Each agent
needs a name and a description, and its prompt body resolves to agents/<name>.md in the
manifest folder. Bun Off deploys agents as markdown files with platform-native frontmatter:
| Platform | Target file |
|---|---|
| Claude Code | .claude/agents/<name>.md |
| OpenCode | .opencode/agents/<name>.md |
| Antigravity CLI | .agents/agents/<name>.md |
agents:
- name: reviewer
description: Read-only code reviewer
model: haiku
mode: subagent # OpenCode only; Claude ignores it
permissions:
allow: [{ tool: read }, { tool: grep }]
deny: [{ tool: edit }, { tool: bash }]
Portable models. The optional model field takes either a string or a per-platform map.
A string applies verbatim to every platform; a map supplies a distinct model id per platform,
and a platform absent from the map inherits that platform's default model. This matters
because the platforms name models differently: Claude expects an alias (haiku, opus,
sonnet) or a full id (claude-opus-4-8), while OpenCode expects provider/model-id.
agents:
- name: planner
description: Deep planner
model:
claude: opus
opencode: anthropic/claude-opus-4-8
Bun Off applies no model-name translation: each value passes through to its platform verbatim, so
state the exact id each platform expects. Antigravity subagents always run on the parent's
model, so a model scoped to it is ignored with a warning.
Per-agent permissions. The optional permissions block scopes the agent's tool access,
using the same allow/deny rule shape as the top-level permissions artifact. Claude scopes
agent tools only coarsely: it accepts allow/deny verdicts for known tools but rejects a
per-agent pattern or an ask verdict. Scope those rules to OpenCode with
available_on: [opencode] on the rule. OpenCode renders the full permission block.
Antigravity cannot scope a subagent's tools at all and rejects any per-agent permission rule.
🔌 Plugins
A plugin copies a local file tree into the workspace root. Plugins use a source: local
install spec and a path: relative to the manifest root.
plugins:
my-plugin:
install:
claude:
source: local
path: plugins/my-plugin
Bun Off copies every file under plugins/my-plugin/ verbatim into the workspace, preserving the
directory structure. Each install: block is platform-specific: only the platforms listed
receive the plugin.
Limitation: only the local source is supported. Network or registry-based sources are not
implemented at the moment.
🛠️ Tool installers
mise
The mise: key lists mise.toml files (paths relative to the manifest root). Bun Off writes each
listed file as a drop-in it owns (boff-0.toml, boff-1.toml, ...), so files that
each declare a [tools] table never collide; mise merges every drop-in natively:
| Scope | Target |
|---|---|
| Workspace | <workspace_root>/.config/mise/conf.d/boff-<n>.toml |
| Global | ~/.config/mise/conf.d/boff-<n>.toml |
mise:
- mise/mise.toml
mise auto-loads every file under conf.d/, so this drop-in is non-destructive: it never
touches a hand-authored mise.toml or .config/mise/config.toml. mise reads the Bun Off drop-in
alongside your own config. Bun Off records the drop-in under the tool:mise owner, so a later deploy
that drops mise: reconciles it like any other artifact (see Deploy state & switching
stacks).
Recommended workflow. Use the drop-in to declare the runtimes your MCP servers and plugins
need. Many MCP server commands assume a runtime on PATH (for example uvx, node, python):
pin those in the manifest's mise/mise.toml so deploying the stack also pins its toolchain.
# mise/mise.toml
[tools]
node = "22"
uv = "latest"
python = "3.12"
Bun Off deploys this config, but it does not run mise: the tools install when mise next runs. If
you already enable mise's shell activation, entering the directory installs them automatically.
For an explicit, reproducible install (fresh clones, CI), wire the recipe below.
Installing the tools (recipe)
Combine two hooks so a deploy leaves a working toolchain and each new session stays current:
-
A
post_installlifecycle hook installs the tools once, right after the deploy writes the drop-in. Save it ashooks/mise-install.py:import subprocess from boff.hooks import HookContext ctx = HookContext.from_stdin() root = ctx.scope.workspace_root subprocess.run(["mise", "trust"], cwd=root, check=False) subprocess.run(["mise", "install"], cwd=root, check=False)
hooks: post_install: - script: mise-install # hooks/mise-install.py
-
A
session_startevent hook re-installs on a fresh checkout or after the tool list changes:event_hooks: - name: mise-install event: session_start command: "mise trust 2>/dev/null; mise install 2>/dev/null || true"
This mirrors the project indexing recipe: install once on
deploy, refresh on session_start.
🪝 Lifecycle hooks
Lifecycle hooks run Python scripts before and after the deploy step. They receive a
JSON-encoded HookContext on stdin. They are distinct from event hooks,
which run inside the assistant at runtime.
hooks:
pre_install:
- script: check_deps
post_install:
- script: notify
Each script: name resolves to hooks/<name>.py in the manifest root. Bun Off runs the scripts
with the same Python interpreter it uses, from the root of the bundle that declared them. A hook
inherited through extends: therefore runs with its own bundle as the working directory, not the
extending manifest's, so a hook may rely on paths relative to the bundle it ships with.
Reading hook context
A hook reads its context by importing boff.hooks.HookContext and calling from_stdin():
from boff.hooks import HookContext
ctx = HookContext.from_stdin()
print(f"Phase: {ctx.phase}")
print(f"Platforms: {ctx.platforms}")
print(f"Workspace: {ctx.scope.workspace_root}")
print(f"Operations planned: {ctx.ops_count}")
HookContext fields:
| Field | Type | Description |
|---|---|---|
phase |
HookPhase |
pre_install or post_install |
platforms |
tuple[str, ...] |
Target platforms for this deploy |
scope |
Scope |
Kind and workspace root |
manifest_root |
Path |
Absolute path to the manifest folder |
ops_count |
int |
Number of file/shell operations planned |
Hooks run before (pre_install) the deploy executes operations and after (post_install) it
completes. A non-zero exit from any hook aborts the run with an error. Hooks do not run in
--dry-run mode.
⚡ Event hooks
Event hooks run inside the assistant in response to runtime events: after an edit, after a shell command, when the assistant finishes. You author one shell body against a normalized contract, and Bun Off deploys the platform glue so the same hook runs on every platform.
event_hooks:
- name: lint
event: after_edit
script: lint # shell script at event_hooks/lint
- name: audit
event: after_bash
command: "logger boff: $BOFF_COMMAND" # inline shell body
Each entry needs a name (the deployed script filename), an event from the normalized set
below, and exactly one of command: (inline shell) or script: (a file under event_hooks/).
Optional fields: available_on: and timeout: (honored by Claude and Antigravity; OpenCode has
no per-hook timeout and ignores it).
Normalized events
event: |
Fires | Claude event | OpenCode hook | Antigravity event | Context provided |
|---|---|---|---|---|---|
after_edit |
after a file edit or write | PostToolUse (Edit|Write) |
tool.execute.after |
PostToolUse (four file-writing tools, below) |
BOFF_FILE |
after_bash |
after a bash command | PostToolUse (Bash) |
tool.execute.after |
PostToolUse (run_command) |
BOFF_COMMAND |
on_finish |
when the assistant finishes | Stop |
session.idle |
Stop |
none |
session_start |
when a session starts | SessionStart |
session.start |
not supported | none |
session_end |
when a session ends | SessionEnd |
session.deleted |
not supported | none |
On Antigravity, after_edit matches four file-writing tools:
write_to_file|edit_notebook|propose_code|file_change. Only write_to_file was observed to fire
in testing; the other three are named because a matcher alternative that never fires costs
nothing.
session_end is faithful on Claude (SessionEnd). OpenCode has no native end event, so Bun Off maps
it to the closest signal, session.deleted: it fires when a session is removed, not on every
graceful exit. Prefer session_start for work that must run once per session (the index-warming
recipe below relies on it).
Antigravity CLI has no session lifecycle event of any kind. A session_start or session_end
hook scoped to it is skipped with a warning: scope such hooks with
available_on: [claude, opencode].
Script contract
Bun Off populates the same environment variables on both platforms, so one shell body works everywhere:
| Variable | Value |
|---|---|
BOFF_EVENT |
the normalized event (after_edit, after_bash, on_finish, session_start, session_end) |
BOFF_TOOL |
the underlying tool, lowercased (edit, write, bash); empty when not applicable |
BOFF_FILE |
edited file path for after_edit; empty otherwise |
BOFF_COMMAND |
the command for after_bash; empty otherwise |
Event hooks are side-effect only: Bun Off ignores their output and exit code. They cannot
block or modify a tool call. For blocking, context injection, or any Claude-native hook event,
write a raw hooks block through the settings: passthrough (Claude only).
What Bun Off deploys
| Platform | Deployed |
|---|---|
| Claude Code | per-hook script at .claude/hooks/<name>; a shared dispatcher .claude/hooks/_boff_dispatch.py; a hooks block merged into .claude/settings.json |
| OpenCode | per-hook script at .opencode/hooks/<name>; an auto-loaded plugin .opencode/plugins/boff-hooks.js |
| Antigravity CLI | per-hook script at .agents/hooks/<name>; a shared dispatcher .agents/hooks/_boff_dispatch.py; a .agents/hooks.json entry per hook |
The Claude dispatcher reads the event payload Claude pipes on stdin and exports the BOFF_*
contract; the generated OpenCode plugin reads the equivalent fields from its hook arguments. You
never write platform-specific glue.
The Antigravity dispatcher also re-applies its own tool matcher. Antigravity runs a
PostToolUse handler at invocation boundaries with a null toolCall, without consulting the
matcher in hooks.json, so after_bash would otherwise fire on turns where no command ran, with
an empty BOFF_COMMAND. The dispatcher drops those, so the contract below holds on all three
platforms.
Cross-platform formatting: use the native formatter
Do not configure a formatting event_hook for both platforms. OpenCode formats natively
and more efficiently through its built-in formatter, so a formatting plugin there is
redundant. Instead, scope the hook to Claude and configure OpenCode's formatter through the
settings: passthrough:
event_hooks:
- name: format
event: after_edit
command: "ruff format \"$BOFF_FILE\""
available_on: [claude] # Claude formats via the hook
settings:
opencode:
formatter: # OpenCode formats natively
ruff:
command: ["ruff", "format", "$FILE"]
extensions: [".py"]
OpenCode's built-in formatters are disabled by default; the formatter block enables and
configures them. The $FILE placeholder is OpenCode's own (not the BOFF_FILE contract).
Project indexing recipe (tldr)
Some MCP tools need a project index built before first use and refreshed when the code moves.
llm-tldr is the canonical example: it warms a persistent index (.tldr/) with tldr warm ..
Combine three hooks so the index stays current without manual steps:
- A
post_installhook builds the index once when the stack is deployed (see Lifecycle hooks). - A
session_starthook re-warms when the working tree moved since the last warm. - An
after_edithook feeds in-session edits to the running daemon.
event_hooks:
- name: tldr-warm
event: session_start
script: tldr-warm # event_hooks/tldr-warm
- name: tldr-notify
event: after_edit
command: '[ -n "$BOFF_FILE" ] && tldr daemon notify "$BOFF_FILE" 2>/dev/null || true'
The event_hooks/tldr-warm script guards on the git revision so it only re-indexes after a real
change, and runs in the background so session start never blocks:
#!/bin/sh
marker=".tldr/.boff-warm-rev"
head=$(git rev-parse HEAD 2>/dev/null || echo none)
[ -f "$marker" ] && [ "$(cat "$marker")" = "$head" ] && exit 0
tldr warm . --background
echo "$head" > "$marker"
This pattern generalizes to any tool with a one-time-plus-refresh index: warm on deploy, refresh
on session_start, and keep hot via after_edit.
🎯 Targeting platforms with available_on:
Any artifact accepts an optional available_on: list: rules, skills, slash commands, MCP servers,
agents, and event hooks, plus each individual permission rule (whether top-level or per-agent).
When set, Bun Off only deploys that artifact to the listed platforms. When omitted, Bun Off deploys
the artifact to every platform in the current --platform invocation.
rules:
- name: opencode-specific
available_on: [opencode]
- universal-rule # deploys to all platforms
🖥️ Supported platforms
| Platform | Name flag | CLI binary | Notes |
|---|---|---|---|
| Claude Code | claude |
claude |
Writes to .claude/ in the workspace root |
| OpenCode | opencode |
opencode |
Writes to .opencode/ and merges opencode.json |
| Antigravity CLI | antigravity |
agy |
Writes to .agents/ and generates GEMINI.md |
The CLI binary column is what boff check looks for on PATH before it
verifies a platform. Pass --no-probe to skip that gate.
Antigravity CLI supports a subset of the manifest. It has no workspace target for slash
commands, settings, or permissions, and no session lifecycle events; Bun Off warns and skips each
of those rather than writing a file the tool would silently ignore. boff check reports those
artifacts as dropped and still exits 0: the platform is doing what it declared. See
Known limitations.
♻️ Deploy state & switching stacks
Bun Off keeps track of every boff deploy: it records what it installs and, on the next deploy,
removes whatever the previous deploy left behind that the new manifest no longer produces. You
can therefore switch a project between stacks — for example a design stack and a maintenance
stack with different rules — just by deploying the other manifest. The previous stack's files
are cleaned up instead of piling up.
Bun Off tracks its footprint in <workspace_root>/.boff/state.json, recording for each platform
(and each tool installer) the files it created and the exact JSON keys it merged into shared
files (.mcp.json, .claude/settings.json, opencode.json). On a new deploy in the same project
Bun Off:
- deletes files it created that the new manifest no longer emits, then prunes any directories left empty;
- removes only the JSON keys it previously injected and no longer produces, leaving keys you added by hand untouched.
The .boff/ directory is self-ignored from Git (Bun Off writes a
.boff/.gitignore), since it reflects local install state. Cleanup is
per platform: for instance, deploying with --platform claude never
touches files Bun Off recorded for opencode.
The managed .gitignore block
Every deploy also lists the files it owns in the workspace's root .gitignore, inside a block
delimited by # BEGIN boff-managed and # END boff-managed. Bun Off rewrites that block on each
deploy and leaves the rest of the file untouched, so deployed artifacts stay out of version
control while your own entries survive. Pass --no-ignore to not touch .gitignore. Bun Off does
othing here when the workspace has no .git directory, and boff check never verifies the block.
Commit the block itself if you want the ignore rules shared; delete it and re-deploy to rebuild it.
boff check reads this same state file to tell you what a re-deploy would clean
up, reporting it as stale. It only reads: check never writes state.json or .gitignore.
Switching example
boff deploy ./design-stack --platform claude # install the design rules
# ...later...
boff deploy ./maintenance-stack --platform claude # remove design's rules, install maintenance's
Both stacks are ordinary manifest folders. Only one is active at a time per platform.
Starting from a clean slate
Two flags (also available as the standalone boff clean command) reset a project
before installing:
-
--cleanremoves Bun Off's entire recorded footprint for the project, across every platform and tool it has deployed, then installs fresh. It is non-destructive: files and settings keys Bun Off never wrote are preserved. -
--wipedeletes all the targeted platforms' native configuration files, including hand-authored content, then installs fresh. It prompts for interactive confirmation before deleting.Platform Deleted by --wipeClaude Code .claude/and.mcp.jsonOpenCode .opencode/andopencode.jsonAntigravity CLI .agents/andGEMINI.mdNote that OpenCode's
opencode.jsonis its settings file as well as its MCP file, so wiping OpenCode discards any settings you keep there by hand.
Caveats:
- A merged-file key that Bun Off once owned but you later overrode by hand is removed if Bun Off stops producing it. Keys Bun Off never wrote are always preserved.
- A tool installer's files (such as
mise) are reconciled only while that tool stays in the manifest. If a later stack drops the tool entirely, its previously written file remains; clear it explicitly withboff clean.
🚀 Commands
Global flags
These precede the subcommand:
| Flag | Description |
|---|---|
-v, --verbose |
Print every executed operation, not just the summary |
--version |
Print the installed package version and exit |
-h, --help |
Show usage and exit |
boff deploy
Deploy a manifest to one or more platforms.
boff deploy <manifest> --platform <name> [--platform <name> ...] [--dry-run] [--clean | --wipe] [--no-ignore]
| Argument | Description |
|---|---|
manifest |
Path or Git URL of the manifest directory (must contain boff.yaml). See Reference scheme |
--platform NAME |
Target platform, repeatable (e.g. --platform claude --platform opencode) |
--dry-run |
Print planned operations without writing any files or running hooks |
--clean |
Remove Bun Off's entire recorded footprint for this project before installing (non-destructive: keeps files Bun Off never wrote). See Deploy state & switching stacks |
--wipe |
Delete all the targeted platforms' native configuration files before installing (destructive: removes hand-authored files too). Prompts for interactive confirmation |
--no-ignore |
Do not add the deployed artifacts to the workspace .gitignore |
Example:
boff deploy ./my-stack --platform claude
boff deploy ./my-stack --platform claude --platform opencode --dry-run
boff deploy ./my-stack --platform claude --clean # purge boff's prior footprint, then install
# Deploy a published bundle straight from GitHub, no clone of your own:
boff deploy https://github.com/atom-sw/bun-off-bundles/tree/main/python --platform claude
The manifest argument says what to deploy; the workspace is always the current directory. A relative path therefore resolves against the current directory too.
Bun Off runs pre_install hooks, applies all operations, then runs post_install hooks. Every
deploy also removes whatever a previous deploy left behind that this
manifest no longer produces (see Deploy state & switching stacks).
boff check
Verify that a workspace still matches what deploying this manifest would write. check re-plans
the deploy, compares the plan against what is actually on disk, and reports each artifact. It
never writes anything: not the artifacts, not .boff/state.json, not .gitignore.
boff check <manifest> --platform <name> [--platform <name> ...] [--no-probe]
| Argument | Description |
|---|---|
manifest |
Path or Git URL of the manifest directory (must contain boff.yaml). See Reference scheme |
--platform NAME |
Target platform, repeatable |
--no-probe |
Skip checking that each platform's CLI binary is on PATH. Use this in CI and containers, where the assistants themselves are not installed |
-v |
Also list every artifact that verified ok (a global flag: it goes before the subcommand) |
Each artifact reports one status:
| Status | Meaning | Exit code |
|---|---|---|
ok |
The artifact is on disk exactly as the manifest describes it | 0 |
missing |
Bun Off would write this file, and it is not there | 1 |
drifted |
The file exists but its content, or a key Bun Off owns in it, differs | 1 |
stale |
Something a previous deploy left behind that this manifest no longer produces. Re-deploying removes it | 1 |
dropped |
The platform has no workspace target for this artifact and said so (see Supported platforms) | 0 |
unsupported |
The platform does not support this artifact type at all | 0 |
unverifiable |
The operation runs a command rather than writing a file, so its effect cannot be verified | 0 |
Shared files are verified only on the keys Bun Off owns. .claude/settings.json, .mcp.json,
and opencode.json mix Bun Off's output with your own. check compares only the JSON key paths
Bun Off merged in; keys you added by hand are never reported as drift, and never as stale.
Example:
boff check ./my-stack --platform claude # quiet: only what is wrong, plus a tally
boff check ./my-stack --platform claude --no-probe # do not require the `claude` binary
boff -v check ./my-stack --platform claude # also list every artifact that verified ok
$ boff check ./my-stack --platform claude
✓ sample-stack v0.1.0
Sample manifest exercising every artifact type.
claude (found: /usr/local/bin/claude)
✗ missing agent reviewer .claude/agents/reviewer.md
✗ drifted settings .claude/settings.json
key permissions.allow: expected ['Read'], found ['Bash(rm:*)']
3 ok, 1 missing, 1 drifted
A clean workspace prints the manifest header and the tally, nothing else. Use check in CI to
fail a build whose committed assistant config has drifted from the manifest that generated it:
boff check ./my-stack --platform claude --no-probe || exit 1
boff clean
Remove Bun Off's deployed footprint from a project without installing anything. This is the
standalone form of the deploy --clean / --wipe flags.
boff clean [--platform <name> ...] [--root <dir>] [--wipe] [--dry-run] [--no-ignore]
| Flag | Description |
|---|---|
--platform NAME |
Restrict to these owners, repeatable. Omit to clean every platform Bun Off recorded. Required with --wipe |
--root DIR |
Project root (default: current directory) |
--wipe |
Delete all the targeted platforms' native configuration files (destructive), instead of only Bun Off's recorded files. Prompts for interactive confirmation |
--dry-run |
Print planned operations without applying them |
--no-ignore |
Do not update the workspace .gitignore |
Without --wipe, boff clean is non-destructive: it removes only the files and merged JSON
keys Bun Off itself recorded, preserving anything you authored by hand.
Example:
boff clean # remove boff's whole footprint for this project
boff clean --platform claude # remove only what boff deployed for Claude
boff clean --wipe --platform claude # delete .claude/ and .mcp.json wholesale (asks first)
boff context
The context subcommand moves a project's AI assistant context — instructions, rules, plans,
memory, and session transcripts — between machines or between platforms.
"Context" here means the durable, on-disk state that seeds a session, not the live context window (which is ephemeral and not portable). What each platform exposes differs:
-
Claude Code: author-written instructions (
CLAUDE.md,.claude/rules/) and Claude's auto-memory store (~/.claude/projects/<encoded-path>/memory/). Export captures both; the auto-memory files travel in the bundle'smemory/directory. -
OpenCode: instructions only (
AGENTS.md,opencode.jsoninstructions). OpenCode has no native memory layer, so an OpenCode bundle carries no memory. Onmigrate, Claude's auto-memory folds into a handoff rule the target auto-loads (seeboff context migratebelow). -
Antigravity CLI: instructions only, carried in
AGENTS.md. Its conversations live in a SQLite store with no export command, so an Antigravity bundle carries no sessions and no memory. Migrating to Antigravity overwritesAGENTS.md(as it overwritesCLAUDE.mdon Claude), inlining the source's primary doc, its rules, and the handoff digest into that one file, since Antigravity reads rules from nowhere else. The deploy-generatedGEMINI.mdtravels along as a config file and is restored onimport, butmigratedrops it: its content comes from the manifest, so redeploy rather than migrate it.Antigravity is the one platform where
boff deployandboff contextwould otherwise target the same file. Deploy ownsGEMINI.md; context ownsAGENTS.md. Antigravity merges both at session start, so nothing is lost, and neither command can clobber the other's work.
boff context export
Save the current project's context to a portable .tar.gz bundle.
boff context export --platform <name> -o <file.tar.gz> [--root <dir>] [--full] [--sanitize]
| Flag | Description |
|---|---|
--platform NAME |
Source platform to collect context from |
-o / --out PATH |
Output archive path (must end in .tar.gz) |
--root DIR |
Project root directory (default: current directory) |
--full |
Include raw session transcripts in the bundle |
--sanitize |
Redact common secret patterns (API keys, tokens, passwords) |
The archive contains instructions, plans, memory files, todo lists, an optional digest summary,
and (with --full) raw session transcripts. It is a human-inspectable gzipped tar with a
manifest.json index.
Example:
boff context export --platform claude -o context-backup.tar.gz
boff context export --platform claude -o context-transfer.tar.gz --sanitize
boff context import
Restore a bundle onto the current machine.
boff context import <bundle.tar.gz> [--into <dir>] [--dry-run]
| Argument | Description |
|---|---|
bundle |
Path to a .tar.gz bundle produced by boff context export |
--into DIR |
Project root to restore into (default: current directory) |
--dry-run |
Print planned operations without applying them |
Bun Off reads the platform from the bundle's embedded manifest and uses that platform's provider
to materialize the files. For Claude Code this re-keys paths to the target machine and restores
memory and plans to the correct locations. For OpenCode, session transcripts import via the
opencode import CLI command.
Example:
boff context import context-backup.tar.gz
boff context import context-backup.tar.gz --into /path/to/project --dry-run
boff context migrate
Convert a project's context from one platform's format to another and write it in place.
boff context migrate --from <source> --to <target> [--into <dir>] [--dry-run]
| Flag | Description |
|---|---|
--from NAME |
Platform to read context from |
--to NAME |
Platform to write context to |
--into DIR |
Project root (default: current directory) |
--dry-run |
Print planned operations without applying them |
Migration remaps the primary instructions file (e.g. CLAUDE.md becomes AGENTS.md), re-roots
rules to the target platform's rules directory, carries over plans, and folds any
platform-specific data that the target cannot store natively (such as Claude's auto-memory)
into a handoff/migrated-context.md rule that the target platform auto-loads.
Example:
# Hand off a Claude Code project to OpenCode
boff context migrate --from claude --to opencode
# Preview what would change
boff context migrate --from opencode --to claude --dry-run
Exit codes
Every command returns one of three exit codes, so you can script against them:
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Error: a bad manifest, corrupt state, a failing or missing hook, or an unreadable file. Bun Off prints a single error: ... line to stderr, never a traceback. A deploy --dry-run that finds a missing hook script also returns 1, and so does a boff check that finds a missing, drifted, or stale artifact |
2 |
Usage error: invalid or missing arguments (for example clean --wipe without --platform) |
⚠️ Known limitations
-
Remote
extends:refetches branch refs. A branch reference is updated on every load; there is no--no-cache/pin-by-default flag yet. Pin to a tag or commit for reproducibility. -
meta.long_descriptionis inline only. It takes a markdown string; pointing it at a separate file is not yet supported. -
Plugin sources: Only
source: localis implemented. Remote or registry-based plugin sources are not yet available. -
Scope: Deploys are workspace-scoped. There is no global install mode, and no CLI flag selects a scope.
-
Antigravity CLI supports a subset of the manifest. It has no workspace target for
slash_commands:,settings:, orpermissions:(its settings and permission allowlist live in the machine-global~/.gemini/antigravity-cli/settings.json), and nosession_startorsession_endevent. Bun Off warns and skips each. It also loads rules only from its primary instructions file, so every rule is inlined into a generatedGEMINI.mdandrules[].categoryandrules[].globshave no effect there. Per-agentpermissions:raise an error, and a subagentmodel:is ignored because Antigravity subagents inherit the parent's model. -
Antigravity's own plugin format is not used. Its
plugin.jsonbundle (skills, agents, commands, MCP servers, and hooks) is discovered in the workspace, but Bun Off deploys those artifacts directly instead. Asource: localplugin still copies its files into the workspace forantigravity, exactly as for the other platforms. -
boff checkcannot verify everything a deploy does. It reads files back, so an operation that runs a command (a plugin installer, a tool installer that shells out) reportsunverifiable. In shared JSON files it compares only the key paths Bun Off owns, so a key it owns that you overrode by hand is not reported until you re-deploy. It does not verify the managed.gitignoreblock. Andboff check --platform antigravityinherits deploy's hard failure on a manifest whose agents carry per-agentpermissions:. -
Antigravity's
GEMINI.mdis Bun Off's.boff deployregenerates it from the manifest, andboff cleanand--wipedelete it, so hand edits there are lost. Put durable instructions inAGENTS.md, which Antigravity merges withGEMINI.mdand which deploy never touches. -
OpenCode full export requires the
opencodeCLI.boff context export --platform opencode --fullcallsopencode exportfor each session. Ifopencodeis not onPATH, sessions are silently skipped. -
--sanitizeis best-effort. The sanitizer redacts known patterns (OpenAI/Anthropic API keys, GitHub tokens, AWS access keys, bearer tokens, and commonkey=valuepairs) but it is not a comprehensive secret scanner. Review exported bundles before sharing. -
Context capture is workspace-scoped. A bundle includes the project's own context only. User-scope and managed-policy instructions (such as
~/.claude/CLAUDE.md) and any@importtargets outside the project root are not captured.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bun_off-0.1.1.tar.gz.
File metadata
- Download URL: bun_off-0.1.1.tar.gz
- Upload date:
- Size: 142.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e015476f82cb66c84eb927c018165d0a4be74bf939782c6ed02590224ff67bf8
|
|
| MD5 |
a3fe417c8930cd017ae7cd6f1a139226
|
|
| BLAKE2b-256 |
cf0eed6596b528c10ac0b8db024b91de7153ce54999274de36073a7001081c60
|
Provenance
The following attestation bundles were made for bun_off-0.1.1.tar.gz:
Publisher:
release.yml on atom-sw/bun-off
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bun_off-0.1.1.tar.gz -
Subject digest:
e015476f82cb66c84eb927c018165d0a4be74bf939782c6ed02590224ff67bf8 - Sigstore transparency entry: 2128319533
- Sigstore integration time:
-
Permalink:
atom-sw/bun-off@74489974784d405b8dbf59d2b98a8d2441932897 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/atom-sw
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74489974784d405b8dbf59d2b98a8d2441932897 -
Trigger Event:
push
-
Statement type:
File details
Details for the file bun_off-0.1.1-py3-none-any.whl.
File metadata
- Download URL: bun_off-0.1.1-py3-none-any.whl
- Upload date:
- Size: 113.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b6669482f51c4252b7afa866fb5d4af47f7e9dff6b9655e32b91b36816519471
|
|
| MD5 |
8bffc075ec48f1cecabd2855733d0408
|
|
| BLAKE2b-256 |
dc171a24992e94b83e5be8dae18794063e4d126bf4d729142cddd2e5a99f87ff
|
Provenance
The following attestation bundles were made for bun_off-0.1.1-py3-none-any.whl:
Publisher:
release.yml on atom-sw/bun-off
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bun_off-0.1.1-py3-none-any.whl -
Subject digest:
b6669482f51c4252b7afa866fb5d4af47f7e9dff6b9655e32b91b36816519471 - Sigstore transparency entry: 2128319563
- Sigstore integration time:
-
Permalink:
atom-sw/bun-off@74489974784d405b8dbf59d2b98a8d2441932897 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/atom-sw
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74489974784d405b8dbf59d2b98a8d2441932897 -
Trigger Event:
push
-
Statement type: