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: tier word | enforced | does not resolve tier words — warns visibly and falls back; a valid slug is honoured | pinned per agent at build time | via runSubagent model param |
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.
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 — 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, tier words are resolved to provider/model-id and pinned per agent, and tools: / disallowedTools: become a permission: block.
The default provider column is GitHub Copilot (enterprise license, flat cost). An Amazon Bedrock alternative (anthropic via AWS_BEARER_TOKEN_BEDROCK) resolves opus → amazon-bedrock/anthropic.claude-opus-4-8, sonnet → amazon-bedrock/anthropic.claude-sonnet-4-6, haiku → amazon-bedrock/anthropic.claude-haiku-4-5-20251001-v1:0 (prefix a region inference profile, e.g. eu./global., if your account requires one).
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
- The Capability Library — the surface × tier model these fields hang off
- How ARCUS Works — location independence and the bootstrap chain