Install Allye for OpenCode
Step-by-step guide to install Allye for OpenCode.
You are an AI agent helping the user install Allye for OpenCode: the Allye MCP server and the allye-opencode plugin, which adds the Allye bootstrap and the Bridge skill. Preserve unrelated MCP servers, plugins and credentials.
Step 1: Resolve the version
Run all the blocks of this guide in one shell session (or as one script): the version resolved here is used by every later block, and each later block starts with a guard that stops when VERSION is missing or malformed. Do this before any change. Set RAW only when you have checked the user's text as a whole against ^v?[0-9]+\.[0-9]+\.[0-9]+$ (digits, dots, optional leading v); otherwise leave it empty to use the latest release of allye-app/allye-plugin. Never paste user text into a command otherwise.
# Rule: never place user text in the script unless it already matched ^v?[0-9]+\.[0-9]+\.[0-9]+$; write only the matched digits and dots (never the original text, never a value containing a quote)
RAW='' # set to the version only if you checked the user's text as a whole against ^v?[0-9]+\.[0-9]+\.[0-9]+$ (digits, dots, optional leading v); otherwise leave empty
[ -n "$RAW" ] || RAW=$(curl -fsSL https://api.github.com/repos/allye-app/allye-plugin/releases/latest | jq -r '.tag_name // empty')
RAW="${RAW#v}"
if ! [[ "$RAW" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "No valid release version could be resolved" >&2; return 1 2>/dev/null || exit 1; fi
VERSION="$RAW"
echo "Installing Allye $VERSION"Any failing block exits the shell session: stop and report to the user; do not continue with later blocks. Tell the user which version you are installing. If no valid version resolves, stop before any change and report it.
Step 2: Check the package exists on npm
If this fails or prints nothing, stop before any change. This checks that the version exists and shows its dist.integrity; it is not a provenance or signature check. It runs from your home directory so a project .npmrc cannot redirect it. Show the user the printed dist.integrity:
: "${VERSION:?run the version step first}"
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Invalid VERSION" >&2; return 1 2>/dev/null || exit 1; }
NPMV=$(cd "$HOME" && npm view allye-opencode@"$VERSION" version) && [ -n "$NPMV" ] || { echo "allye-opencode@$VERSION is not on npm" >&2; return 1 2>/dev/null || exit 1; }
echo "npm has allye-opencode@$NPMV"
(cd "$HOME" && npm view allye-opencode@"$VERSION" dist.integrity)Step 3: Configure the MCP server and the plugin
Merge the Allye entries into the user's OpenCode config and pin the plugin to allye-opencode@$VERSION. Unrelated MCP servers, plugins and settings are kept. Any other allye-opencode plugin entry (unpinned, another version, or array form ["allye-opencode", {...}]) is replaced by the pin; an array-form entry keeps its options and only its first element is re-pinned. A rerun leaves exactly one allye MCP entry and one allye-opencode@$VERSION plugin entry.
Safety rules the block implements:
- If only
opencode.jsoncexists (noopencode.json), it stops and reports; it never createsopencode.jsonnext to it. - The config is validated first: invalid JSON leaves the file byte-for-byte unchanged, nothing is backed up, and you report the parse error to the user.
- The config directory is
$XDG_CONFIG_HOME/opencodewhenXDG_CONFIG_HOMEis an absolute path other than/; an empty, relative or all-slash value counts as unset (trailing slashes are stripped first) and falls back to~/.config/opencode. - A symlinked config is resolved (
readlink -fneeds GNU coreutils or macOS 12.3+), and the file mode is preserved. A dangling symlink atopencode.jsonstops the run. - A
pluginvalue that is not an array makes the transform fail, and the config is left unchanged. - Close OpenCode before running this (a running OpenCode may rewrite the file). A shell killed mid-run can leave a
.opencode.json.XXXXXXtemp file (mode 600) in the config directory; delete it. An existing config is copied first (private, mode 600) to~/.allye/backups/opencode/opencode.json.allye.backup.<UTC ts>, and the new content is transformed from that copy into a temporary file in the same directory. If the copy fails, or the config changed since the copy, the config is left unchanged. The path is reported only after the config was replaced; on a failure after the backup, the message names the kept backup. A new config (none existed) is created private (mode 600). Backups accumulate in~/.allye/backups/opencode/; tell the user where they are so they can prune them. - The pin replaces the existing
allye-opencodeentry in place (same position; appended only when absent). jq rewrites the file, so its formatting is normalised and large numbers or duplicate keys may be normalised too; the backup has the original bytes. Only the file mode is preserved, not ownership or group. - This guide does not check for a downgrade: if the config pins a newer
allye-opencode@<version>, it is replaced by the resolved version. - The legacy
allye-mcpMCP entry is dropped from the config;opencode mcp logout allye-mcp(which clears only that obsolete registration) runs only after the config was replaced, and a logout failure is reported to the user and is not fatal. - An existing
allyeMCP entry is replaced with the Allye default (type, url, enabled); a differing previous entry is replaced and is recoverable from the backup.
: "${VERSION:?run the version step first}"
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Invalid VERSION" >&2; return 1 2>/dev/null || exit 1; }
X="${XDG_CONFIG_HOME:-}"
case "$X" in /*) ;; *) X="" ;; esac
while [ "${X%/}" != "$X" ]; do X="${X%/}"; done
[ -n "$X" ] || X="$HOME/.config"
DIR="$X/opencode"
CFG="$DIR/opencode.json"
if [ ! -e "$CFG" ] && [ -e "$DIR/opencode.jsonc" ]; then
echo "Found opencode.jsonc but no opencode.json: not creating a second config. Report this to the user and stop; a jsonc config must be migrated or edited by hand." >&2
exit 1
fi
if [ -L "$CFG" ] && [ ! -e "$CFG" ]; then echo "opencode.json is a dangling symlink; left unchanged" >&2; exit 1; fi
mkdir -p "$DIR" || { echo "could not create $DIR" >&2; exit 1; }
if [ -e "$CFG" ]; then
CFG=$(readlink -f "$CFG") || { echo "could not resolve opencode.json; left unchanged" >&2; exit 1; }
[ -f "$CFG" ] || { echo "opencode.json is not a regular file; left unchanged" >&2; exit 1; }
fi
B=""
SEED=""
if [ -f "$CFG" ]; then
jq -e . "$CFG" >/dev/null || { echo "opencode.json is not valid JSON; left unchanged" >&2; exit 1; }
BD="$HOME/.allye/backups/opencode"
( umask 077; mkdir -p "$BD" )
chmod 700 "$BD"
B="$BD/opencode.json.allye.backup.$(date -u +%Y%m%dT%H%M%SZ)"
[ -e "$B" ] && B="$B-$$"
( umask 077; cp "$CFG" "$B" ) || { echo "backup failed; left unchanged" >&2; exit 1; }
chmod 600 "$B"
SRC="$B"
else
SEED=$(mktemp); SRC="$SEED"
echo '{"$schema": "https://opencode.ai/config.json"}' > "$SRC"
fi
LEGACY=$(jq -r 'try ((.mcp // {}) | has("allye-mcp")) catch false' "$SRC")
TMP=$(mktemp "$(dirname "$CFG")/.opencode.json.XXXXXX")
jq --arg v "$VERSION" '
def isa: if type == "string" then (. == "allye-opencode" or startswith("allye-opencode@"))
elif type == "array" then (.[0] | type == "string" and (. == "allye-opencode" or startswith("allye-opencode@")))
else false end;
.mcp = (.mcp // {})
| del(.mcp["allye-mcp"])
| .mcp.allye = { "type": "remote", "url": "https://mcp.allye.app/mcp", "enabled": true }
| .plugin = ((.plugin // []) as $p
| if ($p | type) != "array" then error("plugin is not an array") else . end
| ("allye-opencode@" + $v) as $pin
| ($p | map(isa) | index(true)) as $i
| (([$p[] | select(type == "array" and isa)] | first // null) as $a
| if $a == null then $pin else ($a | .[0] = $pin) end) as $new
| if $i == null then $p + [$pin]
else [ $p | to_entries[] | select((.value | isa | not) or .key == $i) | if .key == $i then $new else .value end ] end)
' "$SRC" > "$TMP" || { rm -f "$TMP" "$SEED"; [ -z "$B" ] || rm -f "$B"; echo "Could not transform the config; left unchanged" >&2; exit 1; }
rm -f "$SEED"
if [ -n "$B" ]; then
cmp -s "$CFG" "$B" || { rm -f "$TMP"; echo "opencode.json changed while this ran; left unchanged. Backup kept at $B. Close OpenCode and rerun" >&2; exit 1; }
chmod "$(stat -c %a "$CFG" 2>/dev/null || stat -f %Lp "$CFG")" "$TMP"
fi
mv "$TMP" "$CFG" || { rm -f "$TMP"; echo "could not write the config${B:+; backup kept at $B}" >&2; exit 1; }
[ -z "$B" ] || echo "Backup: $B"
if [ "$LEGACY" = true ]; then opencode mcp logout allye-mcp || echo "Could not log out the legacy allye-mcp; tell the user" >&2; fiReport the backup path to the user when one was created.
The allye entry intentionally has no headers or fixed OAuth client metadata.
Step 4: Authenticate
Restart OpenCode, then:
opencode mcp auth allye
opencode mcp listOpenCode owns discovery, client registration, token storage and refresh.
Step 5: Confirm
Tell the user:
Allye is configured for OpenCode:
- the
allyeMCP server athttps://mcp.allye.app/mcp, using native OAuth;- the
allye-opencode@{version}plugin, pinned to that release, which adds the Allye bootstrap and the Bridge skill (/bridge <mode>).Restart OpenCode to load the plugin. The agent identifies the repository's project with
project_resolve; it asks for a team only when it needs to create a project and you have no default team.
Organization skills
./install.sh install opencode <skill> (see the README) installs into ${XDG_CONFIG_HOME:-~/.config}/opencode/allye-skills. An empty or relative XDG_CONFIG_HOME counts as unset. OpenCode does not discover this directory natively: the allye-opencode plugin adds it to skills.paths when it is a real directory (not a symlink) owned by you and not group- or world-writable. The installer refuses to install into a symlinked or group/world-writable one (fix with chmod go-w <dir>); it does not check ownership, so the plugin additionally ignores a directory that is not owned by you. When the install creates the directory, the installer prints a notice: restart OpenCode to load it.
- Edited copies:
./install.sh install opencode <skill> --reinstallreplaces your edited copy and keeps it at~/.allye/backups/opencode/<skill>.allye.backup.<UTC ts>. A copy of the same release on the same target is already installed (a no-op); older-release copies are updated, and copies recorded for another target (after a hostname or skills-path change) are reinstalled when unchanged, while an edited one needs--reinstall. Foreign or unmanaged folders are never touched. - Leftovers: if the installer reports a
.allye-backup.<skill>.*entry in the skills directory, an earlier run kept your edited folder there. Move it into~/.allye/backups/opencode/or delete it, then rerun. - Any member with view access to a skill can install it. If the installer is too old for the API, upgrade the installer; there is nothing to ask an admin to reset. Until the allye-api PROD release the installer works only against HML. See the README.