Skip to content

Running Across Hosts ​

What ARCUS actually relies on from Claude Code, GitHub Copilot CLI, VS Code Copilot Chat, and OpenCode — and how each one differs


The mindset shift

Host-agnostic does not mean "write it once and hope". Every field in an ARCUS agent's frontmatter is either enforced, ignored, or destructive on a given host — and when a host ignores a field, it does so silently. The rules on this page are all measured, not read from docs, because the docs were wrong about ARCUS and ARCUS was wrong about itself.

The one failure mode that matters ​

Almost every cross-host bug ARCUS has had is the same shape:

A field or instruction that reads as a guarantee and does nothing, with no error anywhere.

Not a crash. Not a warning. The agent runs, produces plausible output, and a safety property you believed in was never in effect. Some real examples, all found by measurement:

What it looked likeWhat actually happened
disable-model-invocation: trueCopilot CLI dropped every agent from its registry — 0 of 16 dispatchable
disallowed-tools: Edit, Writekebab-case is silently ignored; the denylist never fired once
disallowedTools + tools: …, Bashhonoured, and still cosmetic — printf x > f writes
no tools: at allinherited the full session toolset, including edit
"call get_errors to verify"a VS Code-only tool; other hosts skip the step rather than erroring
OpenCode permission: listing only grantsan absent key means allowed — the shell leaked back in
"consult the arcus:model-strategy skill"no Skill in tools:; the agent says it can't, then answers anyway

Each of these now has a static gate in tests/lib/checks.mjs, because on the host ARCUS is developed against, none of them were visible.

What each host enforces ​

layer: is inert on every host — it drives ARCUS's own CI gates, not runtime behaviour.

Frontmatter fieldClaude CodeGitHub Copilot CLIOpenCodeVS Code Copilot Chat
nameenforcedenforcedrewritten flat at build timevia .agent.md conversion
descriptionused for selectionused for selectionused for selectionused for selection
tools: (allowlist)enforcedenforcedtranslated to permission:enforced
disallowedTools:honoured (camelCase only)inferred inert (unmeasured)translated to permission: deny—
disallowed-tools: (kebab)silently ignoredsilently ignoredread with fallback by the adapter—
model:always inherit — no model key emitted; per-dispatch models.mjs resolve is the bindingalways inherit — no model key passed; a valid slug at the call site is honouredalways inherit — the bundle ships no model key; the plugin writes one at load time from your repo's policyvia runSubagent model param, resolved at dispatch
disable-model-invocation:ignored on agents, honoured on skillshonoured on both — removes the item entirely——
layer:inertinertinertinert

Never set disable-model-invocation

Orchestrated dispatch is model invocation — the identical tool call from the identical caller — so the flag cannot mean "orchestrator-only". Setting it made all 16 ARCUS agents undispatchable on Copilot CLI and made model-strategy unloadable on both hosts. Express the intent with user-invocable: false plus an orchestration-scoped description: instead.

Cross-host story continuity

A story scaffolded on Claude Code, continued on Copilot CLI, and finished on OpenCode reads the same frozen model policy and each host picks its own column — no configuration change, no flag, no rescaffold. The resolver emits a host-keyed map; each session reads that map from the story's checkpoint and uses its own key verbatim. See Model Policy for the resolver mechanics.

Addressing an agent ​

Both registry hosts namespace plugin agents by plugin name, so an agent has one literal that works on both:

ReferenceClaude CodeGitHub Copilot CLI
Agent (agents/<name>.md)arcus-plugin:<name>arcus-plugin:<name>
Skill (skills/<name>/SKILL.md)arcus-plugin:<name><name> (bare)

A skill has no single literal that is correct on both hosts — arcus-plugin:model-strategy is an error on Copilot CLI. This is why ARCUS prose writes arcus:<name>: a host-neutral reference token that lets the test harness validate every cross-reference against one spelling.

How a model actually resolves arcus:<name>

Measured on both hosts: the model never parses the prefix. It strips it, takes the bare name, and matches it against the host's live registry enum — the host supplies the namespace. Both hosts resolved arcus:model-strategy correctly on the first try, to two different literals. So the right instruction is "match the name against your registry", never "rewrite the prefix".

Tool names ​

AuthoredClaude CodeCopilot CLI
ReadReadview
GrepGrepgrep
GlobGlobglob
BashBashbash, read_bash, stop_bash, list_bash
Edit / WriteEdit / Writeedit
Task / AgentAgenttask
SkillSkillskill
Sql—sql

Two traps worth authoring around:

  • On Copilot CLI, skill and sql are auto-granted regardless of the tools: allowlist. A declared allowlist is not the agent's whole toolset there: e.g. tools: Read, Grep, Glob yields view, grep, glob, skill, sql total. Neither auto-granted tool can write to the repository.

  • Unknown names are dropped silently. A misspelled or host-specific name narrows the agent's real toolset with no error anywhere.

  • Listing Bash alongside Grep/Glob costs you those two on Claude Code — and only there. Claude Code yields all three for Read, Grep, Glob, but only Read, Bash for Read, Grep, Glob, Bash; the shorter list is the more capable one there. Copilot CLI yields view, grep, glob, bash (+ the read_bash/stop_bash/list_bash siblings) for that same line. Because the hosts disagree, keep Grep/Glob declared even next to Bash: they are live on Copilot CLI and merely inert on Claude Code, so removing them trades real capability for tidiness.

Bash is a write tool

A denylist over Edit/Write/MultiEdit does not make an agent read-only if the allowlist contains a shell — printf x > f writes, git commit rewrites history, rm deletes. A dispatch tool is the same hole one level out, since it can spawn a writer. ARCUS's advisory reviewers therefore allowlist Read, Grep, Glob and nothing else, and receive what they need as prompt input.

When there is no registry entry ​

ARCUS resolves a dispatch target in two routes, using the first that works:

  1. A registered subagent type ending in <name>. Preferred, because the host then enforces the agent's tools: allowlist — this is what keeps the advisory reviewers read-only.
  2. A generic subagent whose prompt opens with "Read and follow the agent spec at $ARCUS_HOME/agents/<name>.md."

Route 2 is needed when no registry entry exists — which is not a per-host property, it is per name:

  • an ARCUS checkout not installed as a plugin (no registry at all);
  • VS Code Copilot Chat, where ARCUS agents are not registered unless converted to .agent.md;
  • a restricted-tools subagent whose own allowlist omits Skill and the dispatch tools — it can resolve nothing, even on a registry host. ARCUS prevents this by construction: gate L1-17 requires Skill in the allowlist of any agent told to consult a skill;
  • a single name missing from an otherwise healthy registry — that one item failed to load.

OpenCode is not in this list. Its bundle registers agents as mode: subagent and rewrites arcus:<name> to the flat name at build time.

Route 2 restrictions are advisory only

Measured on Copilot CLI: a route-2 generic subagent given a spec declaring tools: Read, Grep, Glob reported having bash, apply_patch, task, skill and twenty more. The spec's tools: is not enforced at all — nothing is sandboxing it. Route 2 is a graceful degradation for reachability, not a security boundary. Prefer route 1 wherever a registry entry exists, and treat a fleet that has silently fallen back to route 2 as having lost its read-only guarantees.

OpenCode is a build-time translation ​

OpenCode does not read ARCUS's authoring format directly. plugins/arcus-opencode bundles a converted copy: arcus: prefixes are stripped and tools: / disallowedTools: become a permission: block. Model selection is the one thing that is not baked in — see below.

Models: resolved at plugin load, not at build time ​

OpenCode bakes an agent's model into its markdown frontmatter, so there is no per-dispatch hook to consult the way Claude Code and Copilot CLI have. Plugin load in your repo is the equivalent moment. The published tarball ships every agent with no model: key; then, at load, the plugin runs the same host-agnostic resolver every other host uses against your repo's policy, and writes the resolved provider/model-id into each staged agent under .opencode/agents/.

That means OpenCode reads the same .arcus/config.json models block as every other host — no separate mechanism, no build flag, no republished package.

  • No policy configured? Every agent resolves to inherit, no model: key is written, and ARCUS runs on your OpenCode session's default model. That is the baseline promise, and it needs no setup.
  • Want tiering? Add a tiers object with your opencode column to .arcus/config.json — see Model Policy for the shape. It takes effect on the next session, because the pins are rewritten on every plugin load.
  • Resolution is fail-open: a malformed policy, or a policy naming other hosts but not opencode, leaves the agent on the session default and logs a warning rather than breaking the session.
  • An agent whose complexity ARCUS doesn't recognise is deliberately left unpinned rather than defaulted, so a heavy agent can never quietly end up on the light model.

For a Bedrock backend (AWS_BEARER_TOKEN_BEDROCK), write your own tiers object with Bedrock model IDs in the opencode column. ARCUS ships no built-in Bedrock (or any other) preset; the model IDs for your account are yours to supply.

An absent permission key means allowed

This is the OpenCode-shaped version of the same silent failure. The adapter therefore emits the permission block deny-by-default: the allowlist is authoritative, every key ARCUS knows about is written explicitly allow or deny, and a disallowedTools entry always wins. Emitting only the granted keys would have left bash unspecified — and therefore allowed — on agents whose entire purpose is to be unable to write.

Hooks ​

Every ARCUS entry point runs scripts/locate.sh as its first step, so ARCUS never depends on a hook firing anywhere — it re-stages .arcus/bin/ unconditionally instead. Both Claude Code and Copilot CLI do also fire the bundled SessionStart hook automatically. Confirmed live 2026-08-06: Copilot CLI's plugin-contributed hook invokes its command with cwd defaulting to the plugin's own install directory, not the session's — bootstrap.sh reads the real cwd from the hook's stdin JSON payload (--from-hook, present in both the native camelCase and the "VS Code compatible"/Claude-format payload shapes) and cds there before doing anything else.