Self-hosted URL- and file-to-Markdown service for humans and AI agents - web pages, documents, images, audio, YouTube. PWA + REST + MCP + Claude Code skill, Reddit-aware, refreshable share links.
# Add to your Claude Code skills
git clone https://github.com/AeternaLabsHQ/pullmdLast scanned: 6/11/2026
{
"issues": [
{
"type": "npm-audit",
"message": "express-rate-limit: Vulnerability found",
"severity": "medium"
},
{
"type": "npm-audit",
"message": "fast-uri: fast-uri vulnerable to path traversal via percent-encoded dot segments",
"severity": "high"
},
{
"type": "npm-audit",
"message": "hono: Hono has CSS Declaration Injection via Style Object Values in JSX SSR",
"severity": "medium"
},
{
"type": "npm-audit",
"message": "ip-address: ip-address has XSS in Address6 HTML-emitting methods",
"severity": "medium"
},
{
"type": "npm-audit",
"message": "qs: qs has a remotely triggerable DoS: qs.stringify crashes with TypeError on null/undefined entries in comma-format arrays when encodeValuesOnly is set",
"severity": "medium"
},
{
"file": "README.md",
"line": 181,
"type": "secret-exfiltration",
"message": "Instruction appears to send credentials/secrets to an external endpoint",
"severity": "medium"
},
{
"file": "README.md",
"line": 200,
"type": "secret-exfiltration",
"message": "Instruction appears to send credentials/secrets to an external endpoint",
"severity": "medium"
},
{
"file": "README.md",
"line": 269,
"type": "secret-exfiltration",
"message": "Instruction appears to send credentials/secrets to an external endpoint",
"severity": "medium"
},
{
"file": "README.md",
"type": "secret-exfiltration",
"message": "…and 1 more similar match in this file",
"severity": "low"
},
{
"file": "README.md",
"line": 360,
"type": "dangerous-command",
"message": "Dangerous command (writes to Claude config): \"> rm -rf ~/.claude/\"",
"severity": "medium"
}
],
"status": "WARNING",
"scannedAt": "2026-06-11T08:49:39.927Z",
"npmAuditRan": true,
"pipAuditRan": true,
"promptInjectionRan": true
}See how pullmd compares with popular alternatives.
pullmd is an open-source ai agents skill for AI coding assistants such as Claude Code, Codex CLI, and ChatGPT, built by AeternaLabsHQ. Self-hosted URL- and file-to-Markdown service for humans and AI agents - web pages, documents, images, audio, YouTube. PWA + REST + MCP + Claude Code skill, Reddit-aware, refreshable share links. It has 491 GitHub stars.
pullmd returned warnings in SkillsLLM's automated security scan. It has no critical vulnerabilities, but review the flagged issues in the Security Report section before adding it to your workflow.
Clone the repository with "git clone https://github.com/AeternaLabsHQ/pullmd" and add it to your Claude Code skills directory (see the Installation section above).
pullmd is primarily written in JavaScript. It is open-source under AeternaLabsHQ on GitHub, so you can review or fork the full source.
Yes. SkillsLLM lists many other AI Agents skills you can browse and compare side by side. Open the AI Agents category from the badge at the top of this page, or use the Related Skills and comparison links further down to weigh pullmd against similar tools.
No comments yet. Be the first to share your thoughts!
Requires a passing catalog security scan.
⚠️ Third-Party Software Notice
This skill is third-party open-source software developed and hosted independently on GitHub. SkillsLLM is an informational directory and does not control or maintain the underlying repository.
Any security checks, ratings, or warnings displayed by SkillsLLM are automated and limited in scope. They do not constitute a security certification or guarantee that the software is safe, error-free, or free from malicious code, vulnerabilities, compromised dependencies, or prompt-injection risks.
Review the source code, permissions, dependencies, and configuration before installing or running any third-party skill. Use is at your own risk. To the maximum extent permitted by applicable law, SkillsLLM is not liable for losses arising from third-party software.
Anything in, clean Markdown out. Self-hosted conversion of web pages, documents, images, audio and YouTube videos into token-lean Markdown, for you and for your AI agents.
Quick start · Use with AI agents · API · Configuration · Changelog
Agents are bad at reading the web. Raw HTML burns context on navigation, cookie banners and scripts, and built-in fetch tools often stumble on JavaScript-heavy pages, PDFs and comment threads. PullMD sits in between: one URL or file in, clean Markdown out, with source and metadata in YAML frontmatter instead of the body.
curl "https://pullmd.example.com/api?url=https://example.com/article"
What that saves, measured on three public pages (tokens counted with the
cl100k_base tokenizer, 2026-10-16):
| Page | Plain curl |
PullMD | PullMD with ?query= |
|---|---|---|---|
| Encyclopedia article (long-form) | 99,458 | 12,549 | 662 |
| Developer documentation page | 48,611 | 6,591 | 454 |
| Reddit thread with comments | blocked (bot check), no thread content | 6,381 | 644 |
The curl column is what a naive fetch tool hands an agent. The last
column is query-scoped extraction: only the
sections relevant to a question.
Reads almost anything
Built for agents
?query= returns only the sections relevant to a question, typically 70-95% fewer tokens on long pages/s/:id re-fetches when the cached copy is older than an hour and serves the last good snapshot if the source diesComfortable to self-host
docker compose up, zero config, multi-arch images (amd64/arm64) on Docker Hub and GHCRFour ways in: PWA · REST API · MCP server · Claude Code skill
mkdir pullmd && cd pullmd
curl -O https://raw.githubusercontent.com/AeternaLabsHQ/pullmd/main/docker-compose.yml
docker compose up -d
# → http://localhost:3000
No .env needed: every variable has a sensible default. Add a .env next
to the compose file to override anything (see Configuration).
Then connect your agent: see AI-agent integration.
Upgrading? v3's only breaking change is the clean-body output format (one-line opt-out in
MIGRATION.md). Running behind a reverse proxy? Since v3.13 you needPULLMD_TRUST_PROXY. Full history in the changelog.
docker-compose.yml (zero-config, abridged)services:
pullmd:
image: aeternalabshq/pullmd:latest
container_name: pullmd
restart: unless-stopped
ports:
- "${PORT:-3000}:3000"
environment:
- PUBLIC_URL=${PUBLIC_URL:-http://localhost:${PORT:-3000}}
- TRAFILATURA_URL=http://trafilatura:8001/extract
- PLAYWRIGHT_URL=http://playwright:8002/render
- MARKITDOWN_URL=http://markitdown:8003/convert
- CACHE_DB=/data/cache.db
volumes:
- ./data:/data
networks:
- pullmd-internal
depends_on:
- trafilatura
- playwright
- markitdown
trafilatura:
image: aeternalabshq/pullmd-trafilatura:latest
container_name: pullmd-trafilatura
restart: unless-stopped
networks:
- pullmd-internal
playwright:
image: aeternalabshq/pullmd-playwright:latest
container_name: pullmd-playwright
restart: unless-stopped
networks:
- pullmd-internal
markitdown:
image: aeternalabshq/pullmd-markitdown:latest
container_name: pullmd-markitdown
restart: unless-stopped
mem_limit: ${MARKITDOWN_MEM_LIMIT:-1g}
networks:
- pullmd-internal
networks:
pullmd-internal:
driver: bridge
Abridged for readability — the
docker-compose.ymlin the repo additionally passes every optional.envvariable through to the containers (Reddit credentials, auth, media/OCR keys, YouTube options, output shaping). Use thecurl -Ocommand above rather than copying this block, or.envoverrides beyond the basics won't reach the containers.
Note: the Playwright sidecar adds ~3.7 GB to your image cache (Chromium + Firefox + WebKit binaries from the official Playwright base image). It's optional — leave
PLAYWRIGHT_URLunset and theplaywrightservice block off, and PullMD silently degrades to static extraction with a fallback note in the metadata.
Note: the MarkItDown sidecar is optional. Leave
MARKITDOWN_URLunset and remove themarkitdownservice block to disable document conversion. Web-page URLs always work without it.
Mirror on GHCR:
ghcr.io/aeternalabshq/{pullmd,pullmd-trafilatura,pullmd-playwright,pullmd-markitdown}. Replace theimage:lines if you prefer GitHub's registry.
For deployments behind Traefik with TLS, use docker-compose.traefik.yml
instead. Same images, but with Traefik labels and the proxy external
network. Set HOST_DOMAIN in .env:
curl -O https://raw.githubusercontent.com/AeternaLabsHQ/pullmd/main/docker-compose.traefik.yml
echo "HOST_DOMAIN=pullmd.example.com" > .env
docker compose -f docker-compose.traefik.yml up -d
This file sets PULLMD_TRUST_PROXY=1: Traefik is the only peer (no port is
published), so PullMD takes the client address and the https scheme from
Traefik's forwarding headers.
PullMD ignores X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host
unless PULLMD_TRUST_PROXY says the connecting peer is a proxy. Behind nginx,
Caddy, a tunnel or a load balancer, set:
PULLMD_TRUST_PROXY=1 # one proxy hop, port not reachable directly
# or: PULLMD_TRUST_PROXY=172.18.0.0/16 # the proxy's address/network
PUBLIC_URL=https://pullmd.example.com
A hop count trusts whatever connects, so use it only when the container port is
not published to anyone but the proxy; otherwise list the proxy's address.
Without the setting, all clients share one rate-limit bucket (login, signup,
share-link misses, OAuth, media budget) and PullMD logs a one-time warning.
PUBLIC_URL keeps the session cookie Secure and is accepted as this site's
host by the login form check. Do not send Referrer-Policy: no-referrer for
PullMD's pages: browsers then post forms with Origin: null, which is refused.
See MIGRATION.md.
git clone https://github.com/AeternaLabsHQ/pullmd.git
cd pullmd
npm install
npm start # http://localhost:3000
npm test # node --test
All variables go in .env (copy from .env.example):
v3.0.0 output format change: the markdown body is clean by default - just
# Titlefollowed by content. The source URL, fetch date, and all extraction metadata remain in the YAML frontmatter unchanged - the body no longer duplicates them. SetPULLMD_SOURCE_HEADER=trueto restore the old inline header. UsePULLMD_FRONTMATTER_FIELDSto pick which frontmatter fields are emitted (handy for trimming tokens in agent pipelines).
| Variable | Required | Purpose |
|---|---|---|
HOST_DOMAIN |
Traefik variant only | Public hostname without scheme. Used by Traefik routing and as fallback for PUBLIC_URL. Unused by the default compose. |
PUBLIC_URL |
no | Full public origin embedded in /help and the skill zip. Defaults to https://${HOST_DOMAIN}. An https:// value also marks the session cookie Secure, and its host |