Turn work URLs into LLM-ready Markdown.
📖 中文文档
GitHub PRs, Slack threads, Confluence pages, and Jira issues — one command, one Markdown file you can save, diff, or feed to a model.
ctxd ships a companion skill at skills/ctxd/SKILL.md that works with both Claude Code and Codex CLI. It teaches the agent to reach for ctxd whenever a supported URL appears in the conversation, instead of falling back to chat-style fetching or in-model connectors.
# Claude Code
mkdir -p ~/.claude/skills && ln -s "$(realpath skills/ctxd)" ~/.claude/skills/ctxd
# Codex CLI
mkdir -p ~/.codex/skills && ln -s "$(realpath skills/ctxd)" ~/.codex/skills/ctxdThe skill assumes Required config is already set up — if credentials are missing, the agent will tell you which key it needs before attempting a fetch.
- CLI-first, not chat-first — one command produces a stable Markdown artifact you can inspect, diff, archive, or feed into any model. No drip-feeding through many tool calls.
- Comments and metadata stay attached — PR reviews and inline threads, Slack threading, Confluence attachments and page metadata, Jira custom fields all preserved.
- Bulk export is the default — page trees, long threads, heavy PRs come out as one file. Parallel fetch across all sources (see Performance for numbers).
- Never silent on data loss — fetch failures, skipped items, and truncations always warn on stderr and show up in the run summary /
manifest.json, even under-q.
| Situation | ctxd CLI |
In-model connector |
|---|---|---|
| Export a whole Confluence page tree | Best fit | Usually many tool calls |
| Dump a long Slack thread for later summarization | Best fit | Usually repeated fetch + resolution |
| Save a PR review artifact to disk | Best fit | Usually no persistent artifact |
# GitHub PR -> markdown file
ctxd -O https://github.com/owner/repo/pull/123
# Slack thread -> stdout
ctxd https://app.slack.com/client/T.../C.../thread/C...-1234567890.123456
# Confluence page tree with images -> local directory
ctxd https://your-site.atlassian.net/wiki/spaces/SPACE/pages/123456 -r -i -O
# Jira issue with referenced images -> local file
ctxd https://your-site.atlassian.net/browse/PROJECT-123 -i -O| Source | URL Pattern |
|---|---|
| GitHub PR | https://github.com/<owner>/<repo>/pull/<number> |
| Slack Thread | https://*.slack.com/archives/... or .../client/.../thread/... |
| Confluence | https://*.atlassian.net/wiki/... |
| Jira | https://*.atlassian.net/browse/<KEY> |
brew tap cheerchen/tap
brew install ctxdcd ctxd
uv sync --group devBefore an agent can actually fetch anything, the relevant auth must already exist.
Config file:
~/.config/ctxd/configTypical keys:
SLACK_TOKEN=xoxp-...
GITHUB_TOKEN=ghp_...
CONFLUENCE_BASE_URL=https://your-site.atlassian.net
CONFLUENCE_EMAIL=you@example.com
CONFLUENCE_API_TOKEN=your-tokenEvery key can also come from the environment, which takes precedence over the
file. chmod 600 the file — ctxd warns on stderr if it is group/world readable.
| Option | Description |
|---|---|
-o, --output <path> |
Write to file/directory (default: stdout) |
-O, --auto-output |
Auto-generate output path by source (mutually exclusive with -o) |
-f, --format text|md |
Output format (default: md) |
-q, --quiet |
Suppress progress logs only — warnings and the completeness summary always print (auto-enabled when stderr is not a TTY) |
-v, --verbose |
Verbose logging |
--profile |
Print stage / HTTP timing summary |
--max-concurrency <N> |
Cap parallel work across fetchers (default: 5) |
--recurse-depth <N> |
Cross-source recursion: expand supported URLs found in output (default: 0=off, max 2; opt-in with 1/2) |
--no-recurse |
Disable cross-source recursion (equivalent to --recurse-depth 0; kept for explicitness) |
--max-chars <N> |
Cap output characters (default: 100000 for stdout; file output unlimited unless set explicitly; -1 = unlimited) |
--max-file-size <N> |
Cap per-attachment download size in bytes (default: 52428800 = 50 MiB; -1 = unlimited) |
--max-run-size <N> |
Cap total attachment download size per run in bytes (default: 524288000 = 500 MiB; -1 = unlimited) |
Options can be placed before or after the URL (e.g. both ctxd -q <url> and ctxd <url> -q).
Export PR metadata, reviews, inline comments, timeline comments, and code changes.
A GitHub token in GITHUB_TOKEN (env or ~/.config/ctxd/config). Create a
classic PAT with the repo scope:
GITHUB_TOKEN=ghp_...repo covers every repository you have access to plus all public ones. For a
repository in a SAML-protected organization, also authorize the token for that
org from the token list page, or the API returns 404 as if the repo did not
exist.
Avoid fine-grained PATs here: they are limited to a single resource owner, so a token scoped to one organization cannot read public repos elsewhere.
ctxd https://github.com/owner/repo/pull/123
ctxd https://github.com/owner/repo/pull/123 -o pr-123.md
ctxd -O https://github.com/owner/repo/pull/123| Option | Description |
|---|---|
-d, --diff-mode full|compact|stat |
Diff output mode (default: compact) |
--clean-body / --no-clean-body |
Strip bot-injected HTML noise from PR body (default: on) |
--no-bots |
Drop bot-authored reviews and comments (default: keep all bots) |
Export a full Slack thread with username resolution and attachments.
Requires a Slack User Token (xoxp-...) with the following scopes:
channels:history,groups:history,im:history,mpim:historyusers:readfiles:read(if downloading attachments)
Obtain at: api.slack.com/apps → Your App → OAuth & Permissions → User Token.
Configure the token (pick one):
# Option 1: Environment variable
export SLACK_TOKEN="xoxp-..."
# Option 2: Config file
mkdir -p ~/.config/ctxd
echo 'SLACK_TOKEN=xoxp-...' >> ~/.config/ctxd/config# New URL format
ctxd https://app.slack.com/client/T.../C.../thread/C...-1234567890.123456
# Archive URL format
ctxd https://your-workspace.slack.com/archives/C.../p...?thread_ts=...When you copy a link to a specific reply (archive URL with ?thread_ts= where the path ts differs from the thread root), ctxd fetches the entire thread but highlights the focused message — a **Focused Message:** line in the header and a ▶ marker on the corresponding message in the conversation flow.
| Option | Description |
|---|---|
--download-files |
Download attachments next to the output as attachments/IMG_{file_id}.{ext} (e.g. IMG_F0AAAAAAA1.png); requires -o/-O and the files:read scope on the Slack token |
--raw |
Keep original Slack mrkdwn markup |
Each attachment line keeps the Slack permalink (the address a human opens) and, when the file was downloaded, appends — saved: attachments/IMG_<id>.<ext> so the local copy is discoverable from the export. The run summary reports how many files were downloaded, or how many were left behind when the flag is not used.
Export Confluence pages to Markdown. By default prints a single page to stdout; pass -r / -i with -o <dir> (or -O) to opt into recursive export with images.
Requires an Atlassian API Token. Obtain at: id.atlassian.com/manage-profile/security/api-tokens.
Configure (all three values are required). Pick one:
# Option 1: Environment variables
export CONFLUENCE_BASE_URL="https://your-site.atlassian.net"
export CONFLUENCE_EMAIL="you@example.com"
export CONFLUENCE_API_TOKEN="your-token"
# Option 2: Config file (recommended for persistent use)
mkdir -p ~/.config/ctxd
cat >> ~/.config/ctxd/config <<'EOF'
CONFLUENCE_BASE_URL=https://your-site.atlassian.net
CONFLUENCE_EMAIL=you@example.com
CONFLUENCE_API_TOKEN=your-token
EOF
chmod 600 ~/.config/ctxd/configEnvironment variables take precedence over the config file, so CI and one-off overrides work unchanged. If the file is readable by group/others, ctxd prints a one-shot stderr warning with the exact chmod 600 command to fix it.
# Default: single page to stdout
ctxd https://your-site.atlassian.net/wiki/spaces/SPACE/pages/123456
# Tiny link (short URL) — resolved via authenticated redirect
ctxd https://your-site.atlassian.net/wiki/x/ABC123
# Recursive export with images, to an explicit directory
ctxd https://your-site.atlassian.net/wiki/spaces/SPACE/pages/123456 -r -i -o ./output
# Or let ctxd pick the output directory name
ctxd https://your-site.atlassian.net/wiki/spaces/SPACE/pages/123456 -r -i -ONote:
-r/-i/--all-attachmentsrequire-o <dir>or-O(Confluence writes a directory tree / images to disk). Tiny links:/wiki/x/<token>URLs are followed once (after auth) and rewritten to the canonical long URL before any page fetch;-Oauto-output names themconfluence-<token>since the real page id isn't known at filename time.
| Option | Description |
|---|---|
-r, --recursive / --no-recursive |
Include child pages (default: off) |
-i, --include-images / --no-include-images |
Download referenced images (default: off) |
--all-attachments |
Download all attachments (default: only referenced images) |
--debug |
Save raw HTML for debugging |
Images that are not downloaded are not dropped: they link to the attachment's REST download URL, which works with the same API token (curl -u "$CONFLUENCE_EMAIL:$CONFLUENCE_API_TOKEN" -L <url>) and embeds no expiring token. The run summary reports how many images were left remote.
Export full Jira issue content (description, comments, custom fields, attachments).
Shares authentication with Confluence (see above), so the same config applies. Jira also supports --debug for raw HTML saving.
ctxd https://your-site.atlassian.net/browse/PROJECT-123
ctxd https://your-site.atlassian.net/browse/PROJECT-123 -o issue.md
# Download every attachment next to the output file
ctxd https://your-site.atlassian.net/browse/PROJECT-123 --all-attachments -O
# Only the images the issue body actually references
ctxd https://your-site.atlassian.net/browse/PROJECT-123 -i -ORich-text custom fields are exported as Markdown. Serializable plain fields (strings, numbers, booleans, simple lists/dicts) are exported too. Unsupported nested objects are omitted with a stderr warning and a note in the run summary — never dropped silently.
Every issue export ends with an Attachments section listing filename, MIME type and size, so nothing is invisible even when no file is downloaded. Attachments are only written to disk when -i or --all-attachments is passed:
| Option | Description |
|---|---|
-i, --include-images |
Download images referenced from the issue body (default: off) |
--all-attachments |
Download every attachment on the issue (default: off) |
Downloaded files go to <output-stem>_attachments/, named <attachment-id>-<filename> so same-named uploads never overwrite each other. Attachment URLs in the description, comments and custom fields are rewritten to the local relative paths; anything that failed to download keeps its original remote URL.
Note:
-i/--all-attachmentsrequire-o <file>or-O. When an issue has attachments and neither flag is set, the run summary says how many were skipped and which flag to use.
Every run prints a one-line completeness summary to stderr (always visible, including under -q):
ctxd summary: source=jira | fetched=1 | rendered=1 | artifacts=1
Counts cover source resources fetched/rendered, artifacts written, plus skipped / failed / truncated when non-zero. Free-form notes (e.g. omitted custom fields, failed downloads) are listed under the line.
When writing to a file or directory, a machine-readable manifest is written next to the output:
| Output mode | Manifest path |
|---|---|
Single file (-o issue.md / -O) |
issue.md.manifest.json |
| Directory (Confluence tree) | <dir>/manifest.json |
Manifest JSON mirrors the summary (counts, notes, per-item status). Cross-source recursion merges child counts into the root summary; child content is embedded in the same artifact, so artifacts stays 1.
- Atomic writes — text and binary files are written via temp file + rename to avoid truncated artifacts on interrupt.
--max-chars— truncates at a newline boundary, closes open code fences, and appends a truncation notice. Hard cap: output length never exceeds the limit. Default applies to stdout; pass the flag explicitly to also cap file output.- Attachment size caps — streamed downloads enforce per-file (
--max-file-size) and per-run (--max-run-size) budgets shared across the recursion tree. Exceeded downloads warn and count as failed rather than filling the disk.
By default, cross-source recursion is off (--recurse-depth 0). Opt in with --recurse-depth 1 or 2 to have ctxd scan the rendered output for supported URLs (Slack, GitHub PR, Confluence, Jira) and fetch them too, appending the results as a labelled appendix. This means a Slack thread that links a Jira issue and a GitHub PR pulls all three in one command — no follow-up fetches needed.
# Recursion off (default) — primary URL only
ctxd https://app.slack.com/client/.../thread/...
# Enable recursion (depth=1, auto-expands linked supported URLs)
ctxd <url> --recurse-depth 1
# Deeper recursion (max 2)
ctxd <url> --recurse-depth 2Key behaviours:
- Deduplication: the same URL is never fetched twice within one run.
- Cap: at most 5 child URLs are expanded per level (prevents Jira issue-link explosions).
- Graceful skip: if a child URL lacks credentials (e.g. a Slack thread links a Confluence page but no Confluence token is configured), the appendix notes the skip instead of failing.
- Confluence directory export: recursion is disabled when using
-o/-Owith Confluence (which writes a page tree to disk); use stdout for recursion.
| Scenario | Baseline | Post-opt | Improvement |
|---|---|---|---|
| Slack thread | 9.09s | 1.61s | 82.3% |
| Confluence single page + image | 1.88s | 1.74s | 7.4% |
| Confluence recursive + images | 27.13s | 4.04s | 85.1% |
| GitHub PR | 6.75s | 4.15s | 38.5% |