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 like | What actually happened |
|---|---|
disable-model-invocation: true | Copilot CLI dropped every agent from its registry — 0 of 16 dispatchable |
disallowed-tools: Edit, Write | kebab-case is silently ignored; the denylist never fired once |
disallowedTools + tools: …, Bash | honoured, and still cosmetic — printf x > f writes |
no tools: at all | inherited 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 grants | an 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 field | Claude Code | GitHub Copilot CLI | OpenCode | VS Code Copilot Chat |
|---|---|---|---|---|
name | enforced | enforced | rewritten flat at build time | via .agent.md conversion |
description | used for selection | used for selection | used for selection | used for selection |
tools: (allowlist) | enforced | enforced | translated to permission: | enforced |
disallowedTools: | honoured (camelCase only) | inferred inert (unmeasured) | translated to permission: deny | — |
disallowed-tools: (kebab) | silently ignored | silently ignored | read with fallback by the adapter | — |
model: | always inherit — no model key emitted; per-dispatch models.mjs resolve is the binding | always inherit — no model key passed; a valid slug at the call site is honoured | always inherit — the bundle ships no model key; the plugin writes one at load time from your repo's policy | via runSubagent model param, resolved at dispatch |
disable-model-invocation: | ignored on agents, honoured on skills | honoured on both — removes the item entirely | — | — |
layer: | inert | inert | inert | inert |
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:
| Reference | Claude Code | GitHub 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
| Authored | Claude Code | Copilot CLI |
|---|---|---|
Read | Read | view |
Grep | Grep | grep |
Glob | Glob | glob |
Bash | Bash | bash, read_bash, stop_bash, list_bash |
Edit / Write | Edit / Write | edit |
Task / Agent | Agent | task |
Skill | Skill | skill |
Sql | — | sql |
Two traps worth authoring around:
On Copilot CLI,
skillandsqlare auto-granted regardless of thetools:allowlist. A declared allowlist is not the agent's whole toolset there: e.g.tools: Read, Grep, Globyieldsview, grep, glob, skill, sqltotal. 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
BashalongsideGrep/Globcosts you those two on Claude Code — and only there. Claude Code yields all three forRead, Grep, Glob, but onlyRead, BashforRead, Grep, Glob, Bash; the shorter list is the more capable one there. Copilot CLI yieldsview, grep, glob, bash(+ theread_bash/stop_bash/list_bashsiblings) for that same line. Because the hosts disagree, keepGrep/Globdeclared even next toBash: 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:
- A registered subagent type ending in
<name>. Preferred, because the host then enforces the agent'stools:allowlist — this is what keeps the advisory reviewers read-only. - 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
Skilland the dispatch tools — it can resolve nothing, even on a registry host. ARCUS prevents this by construction: gate L1-17 requiresSkillin 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
tiersobject with youropencodecolumn 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.
Related
- Model Policy — configuring which model each dispatch uses, and the resolver's cross-host column map
- The Capability Library — the surface × tier model these fields hang off
- How ARCUS Works — location independence and the bootstrap chain