Skip to main content

Consumption Model

The BeWith engineering platform ships as one Claude Code plugin published from this repo (bewith-dev/bewith-docs). A developer installs it once per machine; from then on every Claude Code session, in any repo, loads the platform's agents, skills, commands, hooks, policies, and team-rules. Git enforcement reaches every repo through a globally configured hooks path. CI mirrors the enforcement so PRs from tools that do not run the local hooks (Copilot, Claude mobile, manual developers) still hit the same gates.

This page is the operational guide โ€” what lives where, what loads when, and how to add a new file without it silently failing to ship. The strategic context (federation as a principle) is in ai-engineering-strategy.md ยง6 and deterministic-ai.md principle 7.

.claude/ is the single source of truth โ€” the plugin root re-exposes itโ€‹

Every artifact lives once, under .claude/ (or policies/, .claude/rules/). Nothing is copied per repo or per tool. The one subtlety is how Claude Code discovers the native components, because the plugin loader and the in-repo loader look in different places:

  • Inside a Claude Code session, project config is read from .claude/ (agents, skills, commands, rules).
  • A plugin's components are discovered at the plugin ROOT (agents/, commands/, skills/, hooks/hooks.json) โ€” not under .claude/.

If we only kept the files under .claude/, the plugin would expose nothing to other repos โ€” which is exactly the bug that shipped silently until #156: claude plugin details bewith showed Skills(0) / Agents(0) / Hooks(0), so every other repo got an empty shell โ€” no session safety, no agents. The fix keeps .claude/ as the source of truth and re-exposes each category at the root with a per-category symlink:

bewith-docs/ (= the plugin root; marketplace source is "./")
โ”œโ”€โ”€ .claude-plugin/
โ”‚ โ”œโ”€โ”€ plugin.json (name: bewith โ€” no version โ†’ SHA-versioned, always-latest)
โ”‚ โ””โ”€โ”€ marketplace.json (name: bewith-dev, plugins[].source: "./")
โ”œโ”€โ”€ .claude/ โ† SINGLE SOURCE OF TRUTH
โ”‚ โ”œโ”€โ”€ agents/ skills/ commands/
โ”‚ โ”œโ”€โ”€ hooks/ (scripts + hooks.json)
โ”‚ โ””โ”€โ”€ rules/team/ (accepted team rules)
โ”œโ”€โ”€ agents โ†’ .claude/agents โ”
โ”œโ”€โ”€ commands โ†’ .claude/commands โ”‚ per-category symlinks: the loader
โ”œโ”€โ”€ skills โ†’ .claude/skills โ”‚ discovers these at the root while
โ”œโ”€โ”€ hooks โ†’ .claude/hooks โ”˜ the files stay under .claude/
โ”œโ”€โ”€ policies/ (Policy Engine YAML โ€” read from here at runtime)
โ””โ”€โ”€ git-hooks/ (pre-commit / pre-push โ€” wired via core.hooksPath)

hooks/hooks.json (reached through the hooks symlink) declares the Claude Code hooks; each entry runs as bash ${CLAUDE_PLUGIN_ROOT}/.claude/hooks/<name>.sh, so it resolves correctly in every repo.

The guarantee against silent breakage: scripts/validate-plugin-symlinks.mjs (wired into pnpm validate, so it runs in CI) fails if any component category under .claude/ lacks its root symlink. The symlinks are per-category, not per-file, so a new agent/skill/command/hook file is exposed automatically โ€” only a brand-new component category needs a one-time new symlink, and the validator is what tells you so.

What loads into context, and whenโ€‹

Shipping everything globally sounds heavy; it is not, because almost all of it is lazy. This is the model to keep in your head โ€” what is read, and what is not:

ComponentExposed viaLoaded into context?When
Agentsagents/ symlinkDescription only (~100 tok) always-on; the body loads when invoked (@agent-X or the auto-router)lazy
Skillsskills/ symlinkname + description always-on; the body loads when the Skill tool invokes itlazy
Commandscommands/ symlinkname + description always-on; the body loads when the user types /cmdlazy
Hookshooks/hooks.jsonZero context cost โ€” they are scripts that run out-of-band on events; only what a hook emits (additionalContext) enters contexton event
Policies (policies/*.yaml)not a native typeNot auto-loaded. The model reads the YAML only when agent-guard / /continue step 4 explicitly reads iton demand
Team rules (.claude/rules/team/*.md)not a native typeIn bewith-docs: Claude Code auto-loads .claude/rules/ natively. In a consumer: the team-rules-inject hook injects themalways-on

In short: always read = every agent/skill/command description plus whatever the SessionStart hooks inject (team-rule guidance, capability awareness, conversation language, and the accepted team rules). Read only on demand = agent/skill/command bodies and the policy YAML. Never read into context = the hook scripts themselves (they execute; only their output may be injected).

Policies and team-rules: the two non-native typesโ€‹

A plugin distributes only agents / skills / commands / hooks natively. Policies and team-rules are not native component types, so they need a deliberate distribution path (#158):

  • Policies โ€” read from the plugin. agent-guard and /continue step 4 resolve the policy directory from the installed plugin โ€” the bewith@bewith-dev entry's installPath in ~/.claude/plugins/installed_plugins.json, plus /policies โ€” with a working-tree fallback inside bewith-docs. The triggered_by globs always match the consumer's changed files; only the policy definitions come from the plugin.
  • Team rules โ€” injected at SessionStart. Claude Code auto-loads a repo's own .claude/rules/** natively, so the rules load for free inside bewith-docs (which is why they work there with no root CLAUDE.md). A consumer has no such files, and @.claude/rules/team/X.md imports are repo-relative (they resolve to nothing in a consumer). The team-rules-inject SessionStart hook reads the accepted rules from ${CLAUDE_PLUGIN_ROOT}/.claude/rules/team/*.md and injects them; it no-ops inside the platform repo to avoid double-injection.

Both mechanisms glob the plugin directory, so a new policy or accepted rule ships automatically on the next plugin update โ€” the same "add a file, it's distributed" guarantee the per-category symlinks give native components.

How a developer installs and updatesโ€‹

StepCommand / mechanismFrequency
Register the pluginbewith platform install (the @bewith-dev/cli) โ€” verifies GITHUB_TOKEN/GH_TOKEN, then merges the bewith-dev marketplace + bewith@bewith-dev into ~/.claude/settings.json. (bewith onboard is the fuller new-machine flow โ€” stack + seed + MCP + menu bar โ€” gated behind a confirmation; use platform install for just the plugin.)Once per machine
Wire git enforcementAutomatic โ€” the plugin's git-hooks-sync SessionStart hook points global core.hooksPath at the plugin's current git-hooks/ and keeps it fresh across always-latest updates. Safe-by-default: no-op on non-bewith repos (bewith-repo-guard.sh), and it won't clobber a custom core.hooksPath (warns; scripts/wire-global-git-hooks.sh --force overrides).Automatic on first session
Scaffold a consumer reposcripts/install-to-consumer.sh โ€” creates CLAUDE.md + .github/copilot-instructions.md + the CI caller if absent; never commitsOnce per repo

The runbook is install-bewith-plugin.md. After install, the developer is set โ€” a repo cloned tomorrow already has enforcement.

Updating is automatic (always-latest). The bewith plugin carries no explicit version in plugin.json or its marketplace entry, so Claude Code resolves the version to the git commit SHA of the GitHub source and auto-updates at session start โ€” every merge to master reaches everyone on their next session, with no manual step (INS-2, adopted 2026-06-08). Because the marketplace repo is private, startup auto-update requires a GITHUB_TOKEN (or GH_TOKEN) in the environment; interactive installs/updates use your existing git credentials instead. Manual update when needed: claude plugin update bewith@bewith-dev โ€” the @marketplace qualifier is required (a bare claude plugin update bewith reports "not found"). There is no built-in release gating: to gate, pin the marketplace entry to a ref (tag or stable branch) instead of tracking master. Acceptable while the platform has no external users; revisit with a staged channel when it does.

Local-marketplace note (platform devs only). A platform dev can instead register bewith-dev as a directory marketplace pointing at the working tree, so claude plugin marketplace update bewith-dev re-reads local edits without a GitHub round-trip โ€” handy for testing plugin changes before merge. The refresh for a directory source is uninstall + reinstall (it does not auto-update). Teammates consuming the GitHub source get always-latest and do not need this.

How each tool reaches the platformโ€‹

ToolMechanism
Claude Code (CLI / desktop / IDE)The installed plugin โ€” components auto-discovered at the root, lazy-loaded per task; hooks declared in hooks/hooks.json; policies + team-rules via the mechanisms above.
GitHub CopilotReads <repo>/.github/copilot-instructions.md, which points at CLAUDE.md and the platform's GitHub URLs. References, not copies.
Claude mobile / Claude.ai webNo hook runtime; suggestions only. CI catches anything that reaches a PR.
Manual developer (no AI)Git hooks fire on commit / push; CI fires on every PR.

How to add a new file safelyโ€‹

This is the practical payoff โ€” what makes "drop a file and trust it ships" true:

  • A new agent / skill / command / hook file โ†’ drop it under .claude/<category>/. It is exposed automatically (the category symlink already exists). pnpm validate confirms the symlink set is intact.
  • A new policy โ†’ drop policies/<name>.yaml. agent-guard globs the directory; no wiring. (validate-policies.mjs checks it against schema.json.)
  • A new accepted team rule โ†’ land it under .claude/rules/team/ via /share-rule; the inject hook globs the directory. Do not add an @-import to the consumer CLAUDE.md โ€” those are repo-relative and broken in consumers.
  • A brand-new component category (the only manual step) โ†’ add the root symlink for it. If you forget, validate-plugin-symlinks.mjs fails the build and tells you exactly which category is unexposed. This is the deliberate backstop: the only thing that can silently fail to ship is caught by CI.

After any of these, bump nothing structural โ€” the distribution is automatic. Consumers pick it up on their next plugin update.

Overlay: repo-specific behaviorโ€‹

Claude Code applies project-level (<cwd>/.claude/) over the plugin by name shadowing โ€” when names collide, project wins; different names both load (Anthropic โ€” sub-agents scope priority).

  • Add a repo-specific agent: drop <consumer>/.claude/agents/<unique-name>.md โ€” loads in addition to the platform's.
  • Override a platform agent for one repo: drop <consumer>/.claude/agents/<platform-name>.md โ€” shadows the platform's version in that repo.
  • For git hooks, a consumer's git-hooks/config.env overrides REQUIRE_* toggles for that repo only.

No custom merge logic โ€” we rely on Claude Code's native scope priority.

CI is the mandatory gateโ€‹

Local hooks fire only for tools that respect them; Copilot, Claude mobile, and manual developers do not. CI is therefore the final enforcement layer. Consumers wire the reusable workflows via their own caller (uses: bewith-dev/bewith-docs/.github/workflows/<wf>.yml@<ref>). The @ref pins which version of the platform's workflow logic the repo runs against โ€” bump to upgrade, change to roll back. See the uniform-CI runbook.

The version contract: the plugin, not a lock fileโ€‹

There is no governance.lock. An earlier design pinned each consumer to a platform version through a per-repo governance.lock.json plus a validator workflow (governance-version-check.yml) and the governance-build.sh SessionStart hook that rebuilt a .claude/effective/ directory. All of it was removed (2026-05-27 for the lock contract; the build hook 2026-06-08) โ€” the audit trail is in glossary.md and ai-engineering-strategy.md ยง6.4โ€“ยง6.5. The version contract is now just two things:

  1. The plugin each developer has installed โ€” always-latest by default (SHA-versioned, auto-updated at startup; INS-2).
  2. The @ref on each consumer's CI wrapper workflow.

The plugin tracks the commit SHA (always-latest); the wrapper @ref pins CI logic per consumer. Together they cover the intent โ€” known state, one-line upgrade, revert-as-rollback โ€” without an extra file or a per-session build step. If audit needs ever require per-repo version recording beyond the wrapper @ref, the decision can be revisited; it would be expressed in plugin terms (a pinned marketplace ref), not a lock file.

What this model does not handleโ€‹

  • Per-repo version pinning of the local Claude Code session. The @ref pins CI; a developer's local session tracks the plugin's latest commit (always-latest, INS-2) โ€” there is no per-repo plugin pin short of a pinned marketplace ref.
  • Subagent nesting. Plugin subagents cannot spawn further subagents; "Handoff Points" in agent files are recommendations to the main session, not in-band Agent-tool calls.
  • Hook scope. Plugin-shipped hooks fire in every Claude Code session on the machine. They must be self-aware โ€” resolve the repo root via ${CLAUDE_PROJECT_DIR} / git and no-op cleanly where they do not apply (e.g. team-rules-inject skipping the platform repo).
  • The claude.ai chat product (web / mobile / Claude desktop chat app). A different runtime from Claude Code โ€” no hook execution, no local filesystem or git working tree, no local scripts. The plugin does not load there; only the knowledge layer can reach it, by re-publishing select skills as account-level claude.ai Skills (org owner uploads a SKILL.md zip). A 27-skill audit found 1 cleanly portable (mermaid-diagrams), 8 viable only as reduced advisory versions via the org's MCP connectors (GitHub/ClickUp/Jira/Confluence/Figma/Slack), and 18 local-by-design (worktrees, local scripts, the sandbox, the hook runtime). The enforcement and automation layer is inherently local. (A Claude Code session steered remotely from the mobile/web app is not this case โ€” it's a real local session, so the plugin runs; only permission dialogs can't be approved remotely.) Because the local gates don't fire off the Claude Code surfaces, CI is the hard floor for this case too โ€” every PR hits the same lint/typecheck/test + branch-name/CU-id/file-size checks regardless of where the work came from (the cloud chat, another AI tool, a no-AI commit), made non-bypassable by branch protection. So it is acceptable that work off the local surfaces is not locally gated; CI still catches it before merge.

See alsoโ€‹