Give opencode the same agent instructions, rules and MCP servers as Claude Code

opencode already reads skills from ~/.claude/skills and ~/.agents/skills, so
those needed nothing. The rest did.

The manifest now links agents/AGENTS.md to ~/.config/opencode/AGENTS.md as
well, beside the Claude Code and Codex names.

opencode keeps its MCP servers in its own JSON config, with no file to point
a symlink at, so link-mcp now writes them there too. It builds one entry per
row of user/agents/mcp/servers with the same tokens it gives Claude Code,
and adds ~/.claude/rules/*.md under instructions, since opencode does not
read that directory by itself. Only those two keys change. The file is
written private and swapped in whole because it now holds tokens, and a
config with JSONC comments is left alone with a warning rather than
rewritten without them.
This commit is contained in:
Gabriel Brown committed 2026-10-07 01:06:44 -04:00
1 parent f4fca1797f
commit 34bc1663ed
3 files changed
+70 -14

No files matched your search

+64 -9
View File
@@ -18,9 +18,15 @@
# This is personal content, so it obeys the same interview decision link-user
# does. A machine that never said yes registers nothing.
#
# Only Claude Code is handled. Codex stores its servers inside config.toml, and
# rewriting a section of somebody's live TOML is a worse failure mode than
# leaving two lines for them to paste once.
# Claude Code and opencode are handled. Codex stores its servers inside
# config.toml, and rewriting a section of somebody's live TOML is a worse
# failure mode than leaving two lines for them to paste once.
#
# opencode has no CLI for this either, but its config is plain JSON, so this
# stage merges two keys into it with jq: one mcp entry per server row, and the
# rules directory under instructions, because opencode reads ~/.claude/CLAUDE.md
# and ~/.claude/skills on its own but not ~/.claude/rules. Every other key in
# that file is left as it was.
set -euo pipefail
@@ -44,8 +50,12 @@ if [[ "$decision" != "yes" ]]; then
exit 0
fi
if ! command -v claude >/dev/null 2>&1; then
log "Claude Code is not installed; nothing to register."
has_claude=false
has_opencode=false
command -v claude >/dev/null 2>&1 && has_claude=true
command -v opencode >/dev/null 2>&1 && has_opencode=true
if ! $has_claude && ! $has_opencode; then
log "Neither Claude Code nor opencode is installed; nothing to register."
exit 0
fi
@@ -62,6 +72,8 @@ fi
registered=0
skipped=0
# The opencode mcp object, built up one server at a time.
opencode_mcp='{}'
while read -r name transport target token_var _rest; do
case "${name:-}" in ''|'#'*) continue ;; esac
@@ -103,6 +115,20 @@ while read -r name transport target token_var _rest; do
fi
fi
auth=""
[[ "${args[0]:-}" == -H ]] && auth="${args[1]#Authorization: }"
if [[ "$transport" == stdio ]]; then
entry="$(jq -cn --arg c "$launcher" --arg t "$token" \
'{type: "local", command: [$c]} + (if $t == "" then {} else {environment: {MCP_TOKEN: $t}} end)')"
else
entry="$(jq -cn --arg u "$url" --arg a "$auth" \
'{type: "remote", url: $u} + (if $a == "" then {} else {headers: {Authorization: $a}} end)')"
fi
opencode_mcp="$(jq -c --arg n "$name" --argjson e "$entry" '.[$n] = $e' <<<"$opencode_mcp")"
$has_claude || continue
# A server already registered exactly as this row would register it is left
# alone. Removing and re-adding it would throw away the OAuth session of a
# server that signs in on its own (bill-tracker, atlassian), so every
@@ -114,8 +140,6 @@ while read -r name transport target token_var _rest; do
'.mcpServers[$n] // {} | {command, env: (if (.env // {}) == {} then null else .env end)}' \
"$HOME/.claude.json" 2>/dev/null || true)"
else
auth=""
[[ "${args[0]:-}" == -H ]] && auth="${args[1]#Authorization: }"
want="$(jq -cn --arg u "$url" --arg a "$auth" \
'{url: $u, auth: (if $a == "" then null else $a end)}')"
have="$(jq -c --arg n "$name" \
@@ -146,5 +170,36 @@ while read -r name transport target token_var _rest; do
fi
done <"$SERVERS"
log "MCP servers: $registered registered, $skipped skipped."
log "Claude Code loads them at start, so restart a running session to pick them up."
if $has_claude; then
log "MCP servers: $registered registered, $skipped skipped."
log "Claude Code loads them at start, so restart a running session to pick them up."
fi
$has_opencode || exit 0
# opencode reads opencode.jsonc or opencode.json, whichever this machine has.
# The file holds tokens once this runs, so it is written private and swapped in
# whole, the way ~/.claude.json already holds them for Claude Code.
OPENCODE_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
opencode_config="$OPENCODE_DIR/opencode.jsonc"
[[ -e "$opencode_config" ]] || opencode_config="$OPENCODE_DIR/opencode.json"
rules="$HOME/.claude/rules/*.md"
mkdir -p "$OPENCODE_DIR"
current='{"$schema": "https://opencode.ai/config.json"}'
[[ -e "$opencode_config" ]] && current="$(cat "$opencode_config")"
# jq cannot read JSONC comments. A file that has them is left alone rather
# than rewritten without them.
if ! merged="$(jq --argjson mcp "$opencode_mcp" --arg rules "$rules" \
'.mcp = ((.mcp // {}) + $mcp) | .instructions = (((.instructions // []) - [$rules]) + [$rules])' \
<<<"$current" 2>/dev/null)"; then
warn "Could not parse $opencode_config as JSON; opencode MCP servers not written."
exit 0
fi
tmp="$(mktemp "$OPENCODE_DIR/.opencode-config.XXXXXX")"
chmod 600 "$tmp"
printf '%s\n' "$merged" >"$tmp"
mv "$tmp" "$opencode_config"
log "opencode: $(jq -r 'length' <<<"$opencode_mcp") MCP servers and the rules written to $opencode_config."
+3 -3
View File
@@ -12,9 +12,9 @@ and one file says where each piece goes.
| Path | Goes to | Why |
| --- | --- | --- |
| `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md` | Two tools, two names, one file. These were byte-identical copies before this, waiting to disagree. |
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | Personal skills live once in the checkout. Both destinations use `linkdir` so Panama's shipped skills can live beside them. |
| `agents/rules/` | `~/.claude/rules` | |
| `agents/AGENTS.md` | `~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`, `~/.config/opencode/AGENTS.md` | Three tools, one file. These were byte-identical copies before this, waiting to disagree. |
| `agents/skills/` | `~/.agents/skills`, `~/.claude/skills` | Personal skills live once in the checkout. Both destinations use `linkdir` so Panama's shipped skills can live beside them. opencode reads both directories itself. |
| `agents/rules/` | `~/.claude/rules` | opencode loads these through `instructions` in its config, which `setup/scripts/link-mcp` writes. |
| `ssh/config` | `~/.ssh/config` | Host aliases only. Keys are per-machine and are never tracked. |
| `espanso/identity.yml` | `~/.config/espanso/match/identity.yml` | Copied, not linked, because a machine may add its own triggers. |
+3 -2
View File
@@ -23,10 +23,11 @@
# Gabriel's agent instructions on their machine.
# ── Agents ───────────────────────────────────────────────────────────────────
# One file, two names. Claude Code reads CLAUDE.md and Codex reads AGENTS.md,
# and before this they were two byte-identical copies waiting to disagree.
# One file, three names. Claude Code reads CLAUDE.md, Codex and opencode read
# AGENTS.md, and before this they were byte-identical copies waiting to disagree.
link agents/AGENTS.md ~/.claude/CLAUDE.md
link agents/AGENTS.md ~/.codex/AGENTS.md
link agents/AGENTS.md ~/.config/opencode/AGENTS.md
# Skills, at both paths that look for them. Each tracked skill is linked from
# this checkout so edits stay shared and `panama update` offers to commit them.