Skip to main content

A tool to configure different agentic coding frameworks consistently across installations.

Project description

🚉 Bun Off

PyPI Python versions CI License

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 git binary on PATH, for manifests that resolve a Git reference and for boff 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:

  1. A path segment ending in .git ends 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 a file:// path.
  2. A browser URL's tree or blob segment (GitLab's /-/tree/) ends it, and the segment after it is the Git ref.
  3. 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 git against the referenced URL, and deploying a manifest runs its pre_install and post_install hooks. 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:

  • agent becomes Task on Claude and task on OpenCode.
  • webfetch with a pattern becomes WebFetch(domain:<pattern>) on Claude, so write the pattern as a bare domain.
  • mcp becomes mcp__<pattern> on Claude, or plain mcp when the rule has no pattern.
  • write collapses into edit on 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_install lifecycle hook installs the tools once, right after the deploy writes the drop-in. Save it as hooks/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_start event 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_install hook builds the index once when the stack is deployed (see Lifecycle hooks).
  • A session_start hook re-warms when the working tree moved since the last warm.
  • An after_edit hook 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:

  • --clean removes 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.

  • --wipe deletes all the targeted platforms' native configuration files, including hand-authored content, then installs fresh. It prompts for interactive confirmation before deleting.

    Platform Deleted by --wipe
    Claude Code .claude/ and .mcp.json
    OpenCode .opencode/ and opencode.json
    Antigravity CLI .agents/ and GEMINI.md

    Note that OpenCode's opencode.json is 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 with boff 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's memory/ directory.

  • OpenCode: instructions only (AGENTS.md, opencode.json instructions). OpenCode has no native memory layer, so an OpenCode bundle carries no memory. On migrate, Claude's auto-memory folds into a handoff rule the target auto-loads (see boff context migrate below).

  • 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 overwrites AGENTS.md (as it overwrites CLAUDE.md on 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-generated GEMINI.md travels along as a config file and is restored on import, but migrate drops it: its content comes from the manifest, so redeploy rather than migrate it.

    Antigravity is the one platform where boff deploy and boff context would otherwise target the same file. Deploy owns GEMINI.md; context owns AGENTS.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_description is inline only. It takes a markdown string; pointing it at a separate file is not yet supported.

  • Plugin sources: Only source: local is 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:, or permissions: (its settings and permission allowlist live in the machine-global ~/.gemini/antigravity-cli/settings.json), and no session_start or session_end event. Bun Off warns and skips each. It also loads rules only from its primary instructions file, so every rule is inlined into a generated GEMINI.md and rules[].category and rules[].globs have no effect there. Per-agent permissions: raise an error, and a subagent model: is ignored because Antigravity subagents inherit the parent's model.

  • Antigravity's own plugin format is not used. Its plugin.json bundle (skills, agents, commands, MCP servers, and hooks) is discovered in the workspace, but Bun Off deploys those artifacts directly instead. A source: local plugin still copies its files into the workspace for antigravity, exactly as for the other platforms.

  • boff check cannot 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) reports unverifiable. 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 .gitignore block. And boff check --platform antigravity inherits deploy's hard failure on a manifest whose agents carry per-agent permissions:.

  • Antigravity's GEMINI.md is Bun Off's. boff deploy regenerates it from the manifest, and boff clean and --wipe delete it, so hand edits there are lost. Put durable instructions in AGENTS.md, which Antigravity merges with GEMINI.md and which deploy never touches.

  • OpenCode full export requires the opencode CLI. boff context export --platform opencode --full calls opencode export for each session. If opencode is not on PATH, sessions are silently skipped.

  • --sanitize is best-effort. The sanitizer redacts known patterns (OpenAI/Anthropic API keys, GitHub tokens, AWS access keys, bearer tokens, and common key=value pairs) 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 @import targets outside the project root are not captured.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bun_off-0.1.1.tar.gz (142.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bun_off-0.1.1-py3-none-any.whl (113.4 kB view details)

Uploaded Python 3

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

Hashes for bun_off-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e015476f82cb66c84eb927c018165d0a4be74bf939782c6ed02590224ff67bf8
MD5 a3fe417c8930cd017ae7cd6f1a139226
BLAKE2b-256 cf0eed6596b528c10ac0b8db024b91de7153ce54999274de36073a7001081c60

See more details on using hashes here.

Provenance

The following attestation bundles were made for bun_off-0.1.1.tar.gz:

Publisher: release.yml on atom-sw/bun-off

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for bun_off-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b6669482f51c4252b7afa866fb5d4af47f7e9dff6b9655e32b91b36816519471
MD5 8bffc075ec48f1cecabd2855733d0408
BLAKE2b-256 dc171a24992e94b83e5be8dae18794063e4d126bf4d729142cddd2e5a99f87ff

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page