Sentry Access: $ARGUMENTS
Single chokepoint for Sentry operations. Caller skills MUST NOT call
mcp__sentry__*, sentry-cli, or https://sentry.io/api/ directly.
Invocation Contract
operation: list-issues org:<ORG> project:<PROJECT> query:<QUERY> [environment:<ENV>]
operation: get-issue issue_id:<ID>
operation: events org:<ORG> project:<PROJECT> query:<QUERY>
operation: releases org:<ORG> project:<PROJECT>
Return parsed JSON in a <result> block.
Substrate Selection
Probe in order — the ordering is the shared credential-substrate-precedence
contract, not a Sentry-local choice. The first tier that is ready and
identity-matches the configured org/project is used; a substrate authenticated
against a different org is skipped, never used.
- Tier 1 — configured-provider substrate:
SENTRY_AUTH_TOKENbearer token against Sentry REST, resolved throughlisa-secrets-access. - Tier 1a —
sentry-cli, if installed and authenticated to the requested org/project from that same token (the CLI arm of the provider substrate). - Tier 2 — interactive MCP fallback: Sentry MCP, if available and
authenticated. Used when tier 1 is genuinely unavailable: no
SENTRY_AUTH_TOKEN, no REST/CLI adapter for the operation, or a Sentry outage.
Sentry documents API auth tokens for REST API calls, and the same token works interactively and headlessly — which is why it leads. The REST tier uses:
# Resolve the token through the chokepoint before giving up on the environment.
# `$SENTRY_AUTH_TOKEN` is the documented fallback, not the only rung: without
# this, a project that keeps its credentials in Bitwarden, Doppler, or AWS has
# no tier 1 path at all and silently resolves through the interactive MCP —
# the exact divergence `credential-substrate-precedence` exists to remove.
# Mirrors `linear-access`, `atlassian-access`, and `notion-access`.
read_sentry_token() {
[ -n "${SENTRY_AUTH_TOKEN:-}" ] && { echo "$SENTRY_AUTH_TOKEN"; return; }
#
# Ordered across trusted machine-managed substrates, ending at the installed
# package. Checkout-local paths are deliberately absent: a familiar generated
# destination is still repository-controlled executable code. The plugin
# rungs are the floor: `resolve-secret.mjs` ships beside this skill, so a rung
# pointing at it is reachable from anywhere the plugin itself is installed.
# Without one, a consumer repository that vendors none of the leading paths
# never reaches a resolver at all — the ladder exits without having asked
# anything, which is what pushed agents into improvising their own credential
# lookups. This LADDER is identical in every skill that resolves a credential
# and `credential-resolver-ladder` fails if any copy diverges. Only what
# happens AFTER the ladder may differ between them.
# Execute only machine-managed plugin/package resolvers. Checkout-local
# candidates are repository-controlled code, not trusted merely by path.
local candidates=()
if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
candidates+=("$CLAUDE_PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
fi
if [ -n "${PLUGIN_ROOT:-}" ]; then
candidates+=("$PLUGIN_ROOT/skills/lisa-secrets-access/scripts/resolve-secret.mjs")
fi
# Last rung deliberately needs no environment variable: an agent that was
# never handed a plugin root still has the installed package to fall back on.
candidates+=(node_modules/@codyswann/lisa/plugins/lisa/skills/lisa-secrets-access/scripts/resolve-secret.mjs)
local resolver
local tried=()
for resolver in "${candidates[@]}"; do
tried+=("$resolver")
if [ -f "$resolver" ]; then
local via_lisa
via_lisa=$(node "$resolver" get SENTRY_AUTH_TOKEN 2>/dev/null) \
&& [ -n "$via_lisa" ] && { echo "$via_lisa"; return; }
# Empty/error means this substrate had no answer; try the next trusted one.
fi
done
# Name every path. A bare `return 1` sends the next reader hunting for a
# resolver they cannot see the absence of; the enumeration turns that into a
# seconds-long diagnosis. Paths and store coordinates only — never any
# resolved value, on any path.
echo "Error: could not resolve SENTRY_AUTH_TOKEN through lisa-secrets-access." >&2
echo "Tried, in order (relative paths are from $PWD):" >&2
printf ' %s\n' "${tried[@]}" >&2
return 1
}
sentry_api() {
local path="$1"
local token
token=$(read_sentry_token) || {
echo "Error: no Sentry auth token. Set SENTRY_AUTH_TOKEN, or store it as" >&2
echo "SENTRY_AUTH_TOKEN in this project's secrets provider." >&2
return 1
}
curl -sS "https://sentry.io/api/0${path}" \
-H "Authorization: Bearer $token"
}
Tier 1a authenticates sentry-cli from the same resolved token
(SENTRY_AUTH_TOKEN=$(read_sentry_token) sentry-cli …) — never from a second
credential store, and never from an interactive sentry-cli login keychain.
If neither tier works, fail with:
Error: no Sentry access substrate available. Authenticate Sentry MCP/CLI or set SENTRY_AUTH_TOKEN.
Mutation boundary
Every operation in the Invocation Contract is read-only — issue, event, and
release retrieval. So the credential-substrate-precedence guarded fallback for
mutating operations (write, read back, assert the tenant from the response, roll
back on mismatch) is not engaged here, and a failed tier is simply skipped. Any
future operation that mutates Sentry state — resolving an issue, editing a
release — is a write and MUST reconcile by read-back under that protocol before
any retry; do not add one to the contract without it.
Invariants
- Tier order is
credential-substrate-precedence:SENTRY_AUTH_TOKENfirst, the Sentry MCP as a preserved first-class fallback. Identity-match against the configured org/project is mandatory on every tier. - The token is resolved through
lisa-secrets-access, with the bareSENTRY_AUTH_TOKENenvironment variable as the documented fallback. Never read a second credential store (OS keychain,~/.sentryclirctoken) directly — the one-store rule lives at the chokepoint. - Org/project come from
.sentryclirc,.lisa.config.json, or explicit operation args; never infer by searching all accessible orgs. - Consumer skills do not embed Sentry REST paths.