Pirate Claw is a local CLI for pulling media candidates from RSS feeds, matching them against your rules, and queueing approved downloads in Transmission.
Phases 01-09 of the current product roadmap are implemented on main. The currently documented engineering epics through Epic 03 are also implemented on main. Further product-surface or delivery-tooling expansion now requires a new planning pass and new approved phase/epic docs.
It currently supports:
- RSS feeds for TV and movies
- title normalization into media metadata
- TV matching with per-title rules
- compact TV config through
tv.defaults + tv.showswith per-show overrides - movie matching with global year, resolution, and codec preferences
- local dedupe and run history in SQLite
- queueing through Transmission RPC
- status inspection and retry of failed submissions
- effective config inspection through
pirate-claw config show - env-backed Transmission credentials via process env or
.env - read-only daemon HTTP API for external consumers when
runtime.apiPortis configured
pirate-claw runpirate-claw daemonpirate-claw statuspirate-claw retry-failedpirate-claw reconcilepirate-claw config show
- Install dependencies with
bun install. - Copy
pirate-claw.config.example.jsonto./pirate-claw.config.json. - Edit your feeds, TV/movie matching rules, and Transmission credentials.
- Make sure the Transmission app is running and local RPC access is enabled.
- Run:
./bin/pirate-claw run --config ./pirate-claw.config.jsonInspect the current state with:
./bin/pirate-claw statusWhen a torrent has been reconciled from Transmission, status shows the latest known lifecycle and brief downloader detail alongside the stored candidate state.
If a tracked torrent later disappears from Transmission before completion, status surfaces it as missing_from_transmission; once a torrent has been observed completed, that completed state stays sticky locally.
Retry failed submissions with:
./bin/pirate-claw retry-failed --config ./pirate-claw.config.jsonReconcile tracked torrents from Transmission with:
./bin/pirate-claw reconcile --config ./pirate-claw.config.jsonInspect the fully normalized effective config with:
./bin/pirate-claw config show --config ./pirate-claw.config.jsonPirate Claw reads a local config file at pirate-claw.config.json by default.
The repo includes a checked-in example at pirate-claw.config.example.json. Your real local config stays untracked.
High-level config shape:
feeds: RSS sources to inspect (optionalpollIntervalMinutesper feed)tv: either the legacy per-show rule array or a compactdefaults + showsobjectmovies: global movie intake policytransmission: local Transmission RPC settings (optionaldownloadDirsfor per-media-type download directories)runtime: daemon scheduling and artifact settings (optional, all fields have defaults;apiPortenables the HTTP API)
Example:
{
"feeds": [
{
"name": "EZTV",
"url": "https://myrss.org/eztv",
"mediaType": "tv"
},
{
"name": "Atlas Movies",
"url": "https://atlas.rssly.org/feed",
"mediaType": "movie"
}
],
"tv": {
"defaults": {
"resolutions": ["720p"],
"codecs": ["x265"]
},
"shows": [
"Beyond the Gates",
{
"name": "The Daily Show",
"matchPattern": "daily show",
"resolutions": ["1080p"]
}
]
},
"movies": {
"years": [2026],
"resolutions": ["1080p"],
"codecs": ["x265"],
"codecPolicy": "prefer"
},
"transmission": {
"url": "http://localhost:9091/transmission/rpc",
"downloadDirs": {
"movie": "/data/movies",
"tv": "/data/tv"
}
},
"runtime": {
"runIntervalMinutes": 30,
"reconcileIntervalMinutes": 1,
"artifactDir": ".pirate-claw/runtime",
"artifactRetentionDays": 7,
"apiPort": 3000
}
}The compact TV form reduces repetition when most tracked shows share one quality policy:
tv.defaultsdefines the sharedresolutionsandcodecstv.showsmay contain plain show names that inherit those defaultstv.showsmay also contain objects with localmatchPattern,resolutions, orcodecsoverrides- the older
tv: [{ ... }]array shape still works unchanged
Pirate Claw expects a reachable local Transmission RPC endpoint.
Before running:
- Open the Transmission app.
- Enable remote access in Transmission settings.
- Confirm the listening port matches your config. The default example uses
9091. - If authentication is enabled, either put the username/password inline in
pirate-claw.config.jsonor setPIRATE_CLAW_TRANSMISSION_USERNAME/PIRATE_CLAW_TRANSMISSION_PASSWORDin a local.env. - If Transmission restricts allowed addresses, keep
127.0.0.1orlocalhostallowed.
Transmission credential precedence is:
- inline
transmission.username/transmission.passwordwin when present - otherwise Pirate Claw reads
PIRATE_CLAW_TRANSMISSION_USERNAME/PIRATE_CLAW_TRANSMISSION_PASSWORD - Pirate Claw loads those env vars from the process environment and from a
.envfile next to your config file
At queue time, Pirate Claw attempts to send Transmission labels based on media type:
moviefor movie feedstvfor TV feeds
If the configured Transmission instance rejects label arguments, Pirate Claw logs a warning and retries the same submission without labels.
The current build is tuned to work against:
https://myrss.org/eztvhttps://atlas.rssly.org/feed
Current behavior:
- queueable torrent URLs come from RSS
enclosure.urlwhen present <link>remains a fallback when no enclosure URL exists- movie items default to
movies.codecPolicy: "prefer", so they can still match when year and resolution fit policy even if codec is missing - explicit preferred codecs still outrank otherwise equivalent unknown-codec movie releases
movies.codecPolicy accepts "prefer" or "require".
Use "require" to reject movie releases that do not expose an allowed codec in the title.
Pirate Claw keeps local operator state out of git:
pirate-claw.config.jsonpirate-claw.db.pirate-claw/runtime/poll-state.json-- persisted feed poll timestamps used by the daemon to resume due-feed scheduling across restarts.pirate-claw/runtime/cycles/-- JSON and Markdown artifacts for daemon cycle results (completed, failed, or skipped for run/reconcile), pruned to 7 days by default
Run the daemon for continuous scheduled operation:
./bin/pirate-claw daemon --config ./pirate-claw.config.jsonThe daemon runs in the foreground, executing run cycles every 30 minutes and reconcile cycles every 1 minute. Stop with Ctrl+C.
When runtime.apiPort is set in the config, the daemon starts a read-only HTTP JSON API alongside the normal scheduling loop:
{
"runtime": {
"apiPort": 3000
}
}When runtime.apiPort is omitted, no HTTP listener starts.
| Endpoint | Description |
|---|---|
GET /api/health |
Uptime, start time, and last run/reconcile cycle snapshots |
GET /api/status |
Recent run summaries from the local database |
GET /api/candidates |
All tracked candidate state records |
GET /api/shows |
TV candidates grouped by show → season → episode |
GET /api/movies |
Movie candidates sorted by title |
GET /api/feeds |
Feed config with poll state and isDue status |
GET /api/config |
Effective config with Transmission credentials redacted |
curl http://localhost:3000/api/health{
"uptime": 3600000,
"startedAt": "2026-04-08T12:00:00.000Z",
"lastRunCycle": {
"status": "completed",
"startedAt": "...",
"completedAt": "...",
"durationMs": 1234
},
"lastReconcileCycle": null
}All endpoints are read-only. No endpoint mutates daemon state. There is no authentication in this version — it is designed for private NAS networks.
Pirate Claw is intentionally still a local operator tool.
Not in scope yet:
- remote feed capture
- hosted persistence
- automatic post-completion file handling
- download renaming or organization rules
- Synology archiving
- broader ingestion redesign
Useful local commands:
bun testbun run test:coveragebun run verifybun run cibun run deliver restackto restack the current delivery ticket after its parent PR was squash-merged tomainbun run closeout-stack --plan <plan-path>to squash-merge a completed stacked delivery phase ontomainin ticket order using forwardgit merge --squash(no rebase)bun run deliver --plan <plan-path> poll-reviewto run the orchestrator's 2/4/6/8-minuteai-code-reviewpolling loop for the active PR and persist reviewed-SHA provenance plus vendor-attributed review artifactsbun run deliver --plan <plan-path> record-review <ticket-id> patched ...to record patched follow-up and make a best-effort attempt to resolve mapped native GitHub inline review threadsbun run deliver ai-reviewto run the same converged post-PR external AI-review lifecycle for a standalone non-ticket PR
The delivery orchestrator applies reviewer-facing guards when opening or editing PR bodies: it rejects escaped-newline sequences, bans auto-generated sections like Summary by ... / Validation / Verification, and rejects basic malformed markdown (mismatched fenced code blocks, bad headings). Literal \\n inside inline code spans is allowed.
The review hooks and triage logic live in ./.agents/skills/ai-code-review/SKILL.md. Ticket-linked delivery PRs and standalone ai-review runs share the same post-PR lifecycle core: polling, outcome accumulation, reviewer-facing metadata refresh, and final persistence. Supported external review agents are CodeRabbit, Qodo, Greptile, and SonarQube. SonarQube uses GitHub check annotations rather than native PR review comments; the fetcher keeps only failed-check annotations so triage stays focused on meaningful static-analysis findings rather than the full warning stream. Repo-level SonarQube scope lives in ./.sonarcloud.properties.
If you are working on the repo rather than just using the CLI, start with docs/00-overview/start-here.md.
Licensed under the GNU General Public License v3.0 or later. See LICENSE.
This project is intended to be free as in freedom, not merely free of charge.