π½ shuck
shuck the husk, keep the kernel.
shuck is a Go CLI that returns the exact failing CI step logs for a pull
request. Instead of clicking through GitHub, the gh CLI, or MCP calls to reach
the one error that matters, shuck drills GitHub Actions failures down to the
failing steps and prints just their error logs. It's built for devs and
agents who want the signal without the fluff.
When CI goes red on a PR, shuck <pr> is the first move. One command takes
you from "a check failed" to the precise error lines β no tab-hopping, no log
scrolling. The Claude Code plugin wires the same
capability in as a skill and an MCP server.
What it does
Given a PR, shuck:
- Resolves the target PR and its head commit.
- Reads the PR's checks via the GitHub API using your
GITHUB_TOKEN.
- Finds the failed GitHub Actions jobs and, within each, the failed steps.
- Downloads only those jobs' logs and extracts the relevant error lines.
- Lists non-Actions failures (external checks / commit statuses) by name β no
logs are available for those.
- Surfaces cancelled jobs and any checks still running, with an upfront
N failed, M cancelled, β¦ summary so nothing is silently dropped.
A local cache under ~/.shuck makes repeat runs cheap: it avoids re-downloading
logs for job attempts it has already inspected on the same commit.
Install
Homebrew (macOS and Linux)
brew install --cask justanotherspy/tap/shuck
Or tap once, then install by short name:
brew tap justanotherspy/tap
brew install --cask shuck
Upgrade with brew upgrade --cask shuck. The cask is republished to
justanotherspy/homebrew-tap
automatically on every release.
Install script
Download a prebuilt binary (no Go toolchain needed). The script picks the
archive for your OS/arch, verifies its checksum, and installs shuck into an
on-PATH directory:
curl -fsSL https://raw.githubusercontent.com/justanotherspy/shuck/main/install.sh | bash
Pin a version or target directory with environment variables:
curl -fsSL https://raw.githubusercontent.com/justanotherspy/shuck/main/install.sh \
| SHUCK_VERSION=v0.2.0 SHUCK_INSTALL_DIR=/usr/local/bin bash
No token is required. The script resolves the latest release via the GitHub
REST API, and if that is unavailable β e.g. a shared/CI egress IP hits the
unauthenticated 60/hr limit and gets a 403 β it falls back to the
github.com releases redirect, which is not rate-limited. To skip discovery
entirely, set SHUCK_VERSION; to lift the API limit, set GITHUB_TOKEN
(or GH_TOKEN).
Or build from source:
go install github.com/justanotherspy/shuck@latest
Binaries are also available on the
releases page (built with
GoReleaser).
Keeping shuck up to date
Check whether a newer release exists, then upgrade in place:
shuck version --check # query GitHub for the latest release
shuck upgrade # download + verify the latest and replace this binary
shuck upgrade replaces the binary wherever it currently lives (the same place
install.sh put it), verifying the download against checksums.txt first. If
shuck was installed with go install, it says so and leaves the upgrade to the
Go toolchain (go install β¦@latest). Plain shuck version is offline; it only
surfaces an "update available" hint from the last --check.
Usage
shuck <owner>/<repo> <pr> # CI + reviews + security for an explicit PR (same as `shuck all`)
shuck <pr-url> # a PR from its GitHub URL
shuck <run-url> # a single GitHub Actions run (CI only)
shuck <job-url> # a single GitHub Actions job (CI only)
shuck <pr> # owner/repo inferred from the local repo's origin
shuck # the open PR for the current branch
shuck --watch [target] # poll until every check finishes, then print the report
shuck logs [target] [--run <id|url>] # (l) failing CI step logs for a PR or a single run
shuck reviews [target] # (r) a PR's reviews and review-comment threads
shuck all [target] # CI + reviews + security (the default)
shuck action <owner>/<action>[@<version>] # (a) resolve an Action to its latest tag + SHA for pinning
shuck security [owner/repo | url] # (s) summarize a repo's security alerts (code scanning, secrets, Dependabot)
shuck setup # install the shuck skill + CLAUDE.md note for Claude Code
shuck version [--check] # print the installed version; --check looks for an update
shuck upgrade # download and install the latest release in place
Running shuck with no subcommand reports a PR's failing CI, its reviews, and
the repo's security alerts together; use logs / reviews (or their l / r
shorthands) to focus on one dimension.
Pass a GitHub Actions URL to skip the PR-wide scan and look at just one run or
job β handy when a CI-failure notification already points at the failing job:
shuck https://github.com/justanotherspy/shuck/actions/runs/123 # whole run
shuck https://github.com/justanotherspy/shuck/actions/runs/123/job/456 # one job
A run/job target reports only that run's Actions jobs (no PR-wide non-Actions
checks) and bypasses the cache, so its logs are always freshly downloaded.
Authentication uses GITHUB_TOKEN (or GH_TOKEN), or pass --token.
export GITHUB_TOKEN=ghp_...
shuck justanotherspy/shuck 42
Flags
| Flag |
Default |
Description |
--context N |
10 |
Lines of context kept around each error match. |
--short-threshold N |
100 |
Logs with at most this many lines are shown whole. |
--tail N |
100 |
Lines tailed when a long log has no error match. |
--pattern RE |
β |
Override the error-matching regexp. |
--full |
false |
Show full, untrimmed logs for failed steps. |
--max-command-lines N |
30 |
Max lines of a failed step's command to show; longer commands are truncated (0 = no limit). |
--token T |
β |
GitHub token (overrides GITHUB_TOKEN/GH_TOKEN). |
--refresh |
false |
Ignore and rebuild the cache. |
--no-cache |
false |
Do not read or write the cache. |
--offline |
false |
Render only from cache, without network access. |
--json |
false |
Emit machine-readable JSON (stable schema) instead of text. |
--version |
false |
Print the shuck version and exit. |
--watch |
false |
Poll until every check reaches a terminal state, then print the report. |
--interval D |
15s |
Poll interval for --watch. |
--watch-timeout D |
0 |
Give up watching after this long (0 = no limit). |
Run shuck --help to print this usage and the full flag list. Flags may appear
before or after the target (shuck owner/repo 42 --json works), and accept one
or two dashes (-json and --json are equivalent). A leading Unicode dash is
tolerated too, so a flag mangled by macOS "smart dashes" or a rich-text
copy-paste (shuck 42 βfull) still works.
Exit codes: 0 no failing checks Β· 1 failing checks reported Β· 2 error.
Cancelled jobs are reported in the summary but do not by themselves set a
non-zero exit code β cancellation is often deliberate (a superseded run, a
manual stop), so it stays 0 unless a real failure is also present.
Watching until CI finishes
--watch turns shuck into a poll-until-complete loop: it re-checks the target
every --interval (default 15s) and returns only when no jobs are still
running β every check has reached a terminal state (success, failure,
cancelled, timed out, β¦) β then prints the final report. The exit code is the
verdict (0 clean, 1 failures, 2 error), so it composes in scripts and
gives an agent a clear "watching is done" signal.
shuck --watch justanotherspy/shuck 42 # wait, then print
shuck --watch --watch-timeout 30m --json <pr-url> # bounded, machine-readable
Progress lines go to stderr; the final report (text or --json) is the only
thing on stdout. Bound an open-ended wait with --watch-timeout D (on timeout,
shuck prints the latest snapshot instead of blocking forever). --watch works
with any target (PR, run, or job) and cannot be combined with --offline, since
the cache does not change while you wait.
Watch keys off "no jobs still running", so if you start it before CI has
registered any runs for the head commit it reports all-clear immediately β start
watching once at least one check exists.
JSON output
--json emits a stable, versioned document instead of the pretty text, so an
agent or script can consume results deterministically. The exit code is
unchanged, so --json still composes in pipelines.
{
"schema_version": 1,
"pr": { "owner": "β¦", "repo": "β¦", "number": 42, "title": "β¦",
"head_sha": "β¦", "head_branch": "β¦" },
"summary": { "failed": 1, "cancelled": 0, "running": 0, "other_failed": 0 },
"failed_jobs": [
{
"id": 7, "run_id": 9, "name": "build", "conclusion": "failure",
"workflow_name": "CI", "workflow_path": ".github/workflows/ci.yml",
"failed_steps": [
{ "number": 3, "name": "Run tests", "kind": "bash",
"command": "go test ./...", "excerpt": "--- FAIL: TestParse β¦" }
]
}
],
"cancelled_jobs": [],
"other_checks": [],
"running_jobs": []
}
For a run/job URL target the pr object is left zero-valued and a run object
carries the head context instead:
{
"schema_version": 1,
"pr": { "owner": "", "repo": "", "number": 0, "title": "", "head_sha": "", "head_branch": "" },
"run": { "owner": "β¦", "repo": "β¦", "run_id": 123, "job_id": 456,
"title": "β¦", "head_sha": "β¦", "head_branch": "β¦", "workflow_name": "CI" },
"summary": { "failed": 1, "cancelled": 0, "running": 0, "other_failed": 0 },
"failed_jobs": [ /* β¦ */ ]
}
schema_version is bumped only on a breaking change; new fields (like run)
are added without a bump. Lists are always present ([], never null).
Pinning GitHub Actions to a SHA
shuck action <owner>/<action> resolves an Action to the latest release tag and
the immutable commit SHA it points to, so you can pin a workflow uses: line to
a SHA (what GitHub and Dependabot recommend) without hunting through the
Releases page:
shuck action actions/checkout # latest stable release
shuck action actions/checkout@v4 # latest v4.x.x
shuck action actions/checkout@4.2 # latest 4.2.x
shuck action actions/checkout 4.2 # version as a separate argument
shuck action github/codeql-action/init # a subpath action resolves its repo's tags
It prints the resolved tag, the SHA, and a ready-to-paste pin line:
actions/checkout
tag: v4.2.2
sha: 08c6903cd8c0fde910a37f88322edcfb5dd907a8
pin: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2
Drop the pin: value straight after uses: in your workflow. The latest
stable release wins; a prerelease (e.g. -rc1) is chosen only when nothing
stable matches. Add --json for a machine-readable document:
{
"schema_version": 1,
"action": "actions/checkout", "owner": "actions", "repo": "checkout",
"requested": "v4", "tag": "v4.2.2",
"sha": "08c6903cd8c0fde910a37f88322edcfb5dd907a8",
"ref": "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8",
"pin": "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v4.2.2"
}
Resolved tags are cached under ~/.shuck/actions/<owner>/<repo> for a day to
avoid re-listing; --refresh re-fetches immediately. Authentication is optional
for public repos β set GITHUB_TOKEN/GH_TOKEN (or --token) to lift the
unauthenticated rate limit.
Security alerts
shuck security [owner/repo | url] pulls a repository's GitHub security alerts
from every available source and summarizes them in one pass β so a human or an
agent can see what to fix without clicking through the Security tab:
shuck security # the repo of the local working directory
shuck security justanotherspy/shuck # an explicit repository
shuck security https://github.com/owner/repo # any github.com/<owner>/<repo>[/...] URL
shuck security --state all owner/repo # include dismissed/fixed/resolved, not just open
shuck security --json owner/repo # the stable JSON document
shuck security --exit-code owner/repo # exit 1 when open alerts are found (CI gating)
It covers three sources:
- Code scanning (e.g. CodeQL) β rule, severity, and
file:line.
- Secret scanning β secret type and the file locations it was found in. The
raw secret value is never fetched or shown, by design.
- Dependabot β the vulnerable package, its ecosystem, the fix version, and
the CVE/GHSA IDs. npm malware advisories surface here too (there is no
separate malware endpoint).
Each source degrades independently: one that is not enabled (or not visible to
your token) is reported and skipped rather than failing the command, so a repo
with only some features enabled still produces output. By default only open
alerts are shown; widen with --state open|all|dismissed|fixed|resolved.
justanotherspy/shuck β security alerts (open)
Summary: 2 alerts β 1 critical, 1 high
Dependabot (2):
β critical npm lodash β 4.17.21 GHSA-jf85-cpcp-j695 CVE-2019-10744
Prototype pollution in lodash
vulnerable: < 4.17.21
manifest: package-lock.json
https://github.com/justanotherspy/shuck/security/dependabot/12
β high pip django β 3.2.4 GHSA-xxxx CVE-2021-33203
Potential directory traversal via admindocs
manifest: requirements.txt
https://github.com/justanotherspy/shuck/security/dependabot/9
Code scanning: not enabled or no access β skipped.
Secret scanning: not enabled or no access β skipped.
Results are cached under ~/.shuck/security/<owner>/<repo> for an hour;
--refresh re-fetches immediately. Security data β especially on private repos β
needs a token (GITHUB_TOKEN/GH_TOKEN, or --token) with the
security_events (or repo) scope. The exit code is 0 on any successful run
and 2 only on an operational error; pass --exit-code to make open findings
exit 1 for CI gating.
For each failed step:
- Short logs (β€
--short-threshold lines) are shown whole.
- Long logs are grepped for error/failure tokens;
Β±--context lines around
each match are kept, with omitted spans marked.
- Long logs with no match are tailed to the last
--tail lines (the
"error only at the very end" case).
Each failed step also shows the command it ran: the full (multi-line) shell
script for a run: step, or the owner/action@ref plus the echoed with:
inputs and env: for an action step. Commands longer than --max-command-lines
(default 30) are truncated with a β¦ (N more lines) β¦ marker; pass
--max-command-lines 0 for no limit.
Example output
justanotherspy/shuck PR #42 β fix flaky parser (commit a1b2c3d)
Summary: 1 failed
Workflow: CI (.github/workflows/ci.yml)
Job: build [failure]
Steps:
1. Set up job (success)
2. Checkout (success)
3. Run tests (failure)
4. Upload coverage (skipped)
βΈ Step 3 β Run tests (failed)
Step command:
* bash run:
```
go test ./...
```
error logs:
```
--- FAIL: TestParse (0.00s)
parse_test.go:42: expected 1, got 2
FAIL
##[error]Process completed with exit code 1.
```
MCP server
shuck doubles as a local Model Context Protocol
server so any MCP-aware agent can pull failing CI logs as typed tool calls
instead of scraping CLI text. Start it over stdio with:
shuck mcp
It exposes four read-only tools:
| Tool |
Purpose |
Key inputs |
inspect_logs |
Failing CI step logs for a PR, or one Actions run. |
repo (owner/repo), pr, url; or none β the open PR for the current branch; or run (a run/job URL, or a bare run ID with repo) |
inspect_reviews |
A PR's reviews and review-comment threads. |
repo (owner/repo), pr, url; or none β the current branch. Optional review_comment_limit |
inspect_security |
A repo's security alerts (code scanning, secrets, Dependabot). |
repo (owner/repo) or url; or none β the local repo. Optional state, refresh |
inspect_action |
Resolve a GitHub Action to its latest tag + commit SHA for pinning. |
action (owner/action[/subpath][@version]). Optional refresh |
inspect_logs accepts the same log-extraction knobs as the CLI (context,
short_threshold, tail, pattern, full) plus the cache flags (refresh,
no_cache, offline). Each call returns the rendered, human-readable report as
text and the matching stable JSON document as typed structured output, so
programmatic consumers get the schema for free. Authentication uses
GITHUB_TOKEN/GH_TOKEN from the server's environment (inspect_action works
unauthenticated against public repos).
Register it with any MCP client. For Claude Code, add it to .mcp.json:
{
"mcpServers": {
"shuck": { "command": "shuck", "args": ["mcp"] }
}
}
The Claude Code plugin registers this server for you; it
runs the shuck on your PATH, so install shuck first (see Install).
Claude Code plugin
shuck also ships as a Claude Code plugin so
agents can pull failing CI logs for you. It adds a /shuck skill, an MCP server
(the inspect_logs / inspect_reviews / inspect_security / inspect_action
tools above) that runs the shuck binary from
your PATH, and a SessionStart hook that checks shuck is installed, recent
enough to run the MCP server, and that a GitHub token is present.
The plugin does not install shuck β install it yourself and keep it
current with shuck upgrade. Then add the marketplace and install the plugin
from within Claude Code:
/plugin marketplace add justanotherspy/shuck
/plugin install shuck@shuck-marketplace
Without the marketplace: shuck setup
Prefer not to use the plugin marketplace? shuck setup wires the same skill in
at the user level:
shuck setup
It:
- installs the
shuck skill into ~/.claude/skills/shuck/SKILL.md (the same
skill the plugin ships, embedded in the binary);
- adds a short, managed note to your
~/.claude/CLAUDE.md saying you can reach
shuck through either the skill (CLI) or the MCP; and
- offers to register the local MCP server at user scope β in an interactive
terminal it prompts; otherwise pass
--mcp to install it (via
claude mcp add --scope user shuck -- shuck mcp) or --no-mcp to skip.
Re-running is safe: the skill and the CLAUDE.md block are refreshed in place, not
duplicated. Writes go under $CLAUDE_CONFIG_DIR (default ~/.claude); use
--dry-run to preview. As with the plugin, install the shuck binary first.
Development
make build # build ./shuck
make test # go test -race ./...
make lint # golangci-lint (run `make lint-install` first)
make cover # coverage report
License
MIT β see LICENSE.