| name | sync-claude-config |
| description | Re-sync this repo's Claude Code config (.claude/agents, .claude/commands, settings.json hooks/permissions) into OpenCode's .opencode/ layout, idempotently. Use after any change to .claude/agents/, .claude/commands/, .claude/settings.json, or .claude/hooks/. |
Sync Claude Code config -> OpenCode
OpenCode already discovers .claude/skills/*/SKILL.md natively, so skills need no migration.
But agents, commands, permissions, and hooks live in Claude-only locations, so they must be
generated into .opencode/. This skill runs the generator that does that — idempotently.
When to use this skill
- After editing any file in
.claude/agents/ (add/remove/rename an agent, change its tools/model).
- After editing
.claude/commands/ (role/command changes).
- After editing
.claude/settings.json (hook list, permission allow-list, skillOverrides).
- After adding/removing a validator in
.claude/hooks/ (these are ported into the plugin).
- Any time you suspect
.opencode/ has drifted from .claude/.
How to run (idempotent)
python scripts/sync_claude_to_opencode.py
The script:
- Reads the source of truth from the committed
.claude/ files only —
the machine-local .claude/settings.local.json is deliberately excluded so
committed artifacts stay reproducible on every machine.
- Rewrites only the
.opencode/ files whose source changed: each source's sha256 is
compared against its entry in .opencode/SYNC_MANIFEST.json and unchanged sources
are skipped (a byte-compare backstops every write).
- Stamps every generated file with a stable, date-free
generated from .claude/...
provenance comment, so unchanged sources produce zero churn on any day.
- Removes orphaned artifacts: a file in
.opencode/agents/ or .opencode/commands/
whose .claude/ source was deleted is removed (deletion logged loudly) and its
manifest entry dropped. Only stamped, generator-written files are eligible —
hand-written files, the skills dir, and the plugin are never touched.
- Advances
generated_on in the manifest only when something actually changed.
Re-running it is safe and produces no churn when sources are unchanged. If you change the
generator script itself (templates, headers), bump GENERATOR_VERSION in it — a version
mismatch in the manifest forces a full refresh pass.
What it generates
.opencode/opencode.json — permission block translated from the committed settings.json
(Bash(cmd:*) -> both "cmd" and "cmd *" glob keys, Skill(name) -> permission.skill,
skillOverrides off -> skill deny). Plus instructions: ["CLAUDE.md"] and model defaults.
.opencode/agents/*.md — one per .claude/agents/*.md. tools: ['*'] -> full access,
explicit tools: lists -> permission allow/deny, disallowedTools -> edit/write deny,
model: sonnet|haiku -> anthropic/claude-*-4-5. builder becomes mode: all; rest subagent.
.opencode/commands/*.md — commands/roles/*.md (excluding _-prefixed includes),
with agent: build and $ARGUMENTS preserved.
.opencode/plugins/valor-bridge.ts — a single OpenCode plugin that re-dispatches the SAME
.claude/hooks/*.py validators on tool.execute.before / tool.execute.after and the nearest
session-lifecycle events (session.created/session.idle/session.compacted).
Blocking honors both Claude Code protocols: stdout JSON {"decision": "block", ...} with
exit 0 (all PreToolUse validators) AND non-zero exit codes (the PostToolUse plan validators'
sys.exit(2)). Lowercase OpenCode tool ids are mapped back to Claude casing (bash -> Bash)
before dispatch. Best-effort (|| true) validators never block. CLAUDE_PROJECT_DIR is
injected from the project directory so validators behave exactly as under Claude Code.
Hooks whose matcher maps to no OpenCode event are reported with a loud stderr WARNING,
never dropped silently.
See docs/features/opencode-sync.md for guarantees and limits (hook-semantics caveats).
Drift detection
.opencode/SYNC_MANIFEST.json records generated_on, generator_version, and the sha256 of
every consumed source file (settings.json, agents, commands, hooks). To check for drift without
changing anything:
python - <<'PY'
import hashlib, json, pathlib
m = json.loads(pathlib.Path(".opencode/SYNC_MANIFEST.json").read_text())
root = pathlib.Path(".")
for section in ("settings", "agents", "commands", "hooks"):
for rel, h in m.get(section, {}).items():
cur = hashlib.sha256((root/rel).read_bytes()).hexdigest()
if cur != h:
print("DRIFT:", rel)
print("generated_on:", m["generated_on"], "generator_version:", m.get("generator_version"))
PY
Any DRIFT line means that source changed and you should re-run the sync. This covers
.claude/settings.json too — permission or hook edits are detectable drift.
Verify after a sync
opencode debug config shows the agents, skills, and resolved permissions.
- Invoke a subagent (e.g.
@validator) — it should be read-only.
- A
git commit with a bad message (e.g. a co-author trailer) is blocked by
valor-bridge.ts: the validator prints {"decision": "block", ...} to stdout and the
plugin throws with the reason.
- Unit tests:
pytest tests/unit/test_sync_claude_to_opencode.py -n0.