Auto-tag untagged bookmarks in GoodLinks using Claude AI. The tool fetches your untagged links from a local GoodLinks API, retrieves each page's content, asks Claude to suggest tags, and applies them back via the localhost API.
Tip
I auto-tagged ~1600 links that I had imported from other bookmarking systems for around $6 USD in Claude AI costs.
- Python 3.14 or later
- uv package manager
- A running GoodLinks instance with API access (local server on port 9428)
- A GoodLinks API token (
GOODLINKS_TOKEN) - An Anthropic API key (
ANTHROPIC_API_KEY)
Clone the repository and let uv handle the dependencies:
git clone <repo-url>
cd goodlinks-tagging
uv syncuv sync reads pyproject.toml and installs the pinned dependencies from uv.lock. The only third-party dependency is anthropic.
| Variable | Required | Description |
|---|---|---|
GOODLINKS_TOKEN |
Yes | Bearer token for the GoodLinks API. Obtain it from your GoodLinks instance (local server on port 9428). |
ANTHROPIC_API_KEY |
Yes | API key for Anthropic's Claude API. Create one at console.anthropic.com. |
Export them before running the scripts:
export GOODLINKS_TOKEN="your-goodlinks-token"
export ANTHROPIC_API_KEY="your-anthropic-key"You must run untagged.py first to fetch untagged links, run get-tags-list.py to get the list of existing tags, then run autotag.py next to process those untagged links. The three scripts must be run in this order because autotag.py reads the output files that untagged.py produces.
Run untagged.py to query the GoodLinks API and save all untagged links to
untagged_links.json:
uv run untagged.pyThis paginates through the API and writes the full list of untagged link objects to disk.
Run get-tags-list.py to fetch your current tag list from GoodLinks and save it as tags.json:
uv run get-tags-list.pyThis gives the script a list of existing tags to stick to. Occasionally it will create new tags (if the AI feels that those new tags are really warranted). You may or may not agree, and you can clean-up or delete those tags once the last script is done running.
Once the other scripts have finished, run autotag.py to process each untagged link:
uv run autotag.pyThe script reads untagged_links.json and tags.json, processes links concurrently, and writes checkpoint progress to progress.json. Already-processed links are skipped automatically, so you can safely re-run the command if it is interrupted.
It tries hard to do a good job and be idempotent. It will also respond to Claude rate limits and back off before triggering HTTP 429 responses.
For each untagged link the pipeline is:
- Fetch page — download the URL and extract visible text (stripping scripts, styles, nav, and other non-content elements).
- Extract text — parse the HTML, pull the
<title>, and truncate the body text toMAX_CONTENT_CHARS. - Ask Claude — send the page text, link metadata, and the current tag list to Claude, which returns a JSON object with
existing_tagsandnew_tags. - Apply tags — write the chosen tags back to GoodLinks via the API. If the page could not be fetched, the link is tagged
"__problem"; if Claude suggests nothing it is tagged"__notags".
Links are processed concurrently using a thread pool (MAX_WORKERS threads). Two sliding-window rate limiters keep requests and token usage within Anthropic's per-minute limits.
All tunable constants are defined at the top of autotag.py. Edit them directly to customise behaviour:
| Constant | Default | Description |
|---|---|---|
GOODLINKS_BASE |
"http://localhost:9428/api/v1" |
Base URL for the GoodLinks API. |
UNTAGGED_FILE |
"./untagged_links.json" |
Path to the untagged links input file. |
TAGS_FILE |
"./tags.json" |
Path to the cached tags file. |
CHECKPOINT_FILE |
"./progress.json" |
Path to the checkpoint file. |
CHECKPOINT_EVERY |
5 |
Write a checkpoint every N processed links. |
MAX_WORKERS |
5 |
Number of concurrent threads. |
MAX_TAGS |
5 |
Maximum number of tags to apply per link. |
FETCH_TIMEOUT |
15 |
HTTP timeout in seconds for fetching web pages. |
MAX_CONTENT_CHARS |
4000 |
Maximum characters of page text sent to Claude. |
LLM_MODEL |
"claude-haiku-4-5-20251001" |
Claude model identifier. |
MAX_RETRIES |
3 |
Maximum retry attempts for Claude API calls. |
Rate limiter defaults (set just below the class definitions in autotag.py):
| Instance | Limit | Description |
|---|---|---|
_req_limiter |
45 requests/min | Sliding-window request rate limiter (hard API limit is 50). |
_token_limiter |
45 000 tokens/min | Sliding-window input-token rate limiter (hard API limit is 50 000). |
(You can delete these when you're done.)
| File | Description |
|---|---|
untagged_links.json |
List of untagged link objects produced by untagged.py. |
tags.json |
Cached snapshot of all GoodLinks tags. Written by get-tags-list.py or refreshed by autotag.py. |
progress.json |
Checkpoint file tracking which link IDs have been processed. |
autotag.py writes a checkpoint to progress.json every CHECKPOINT_EVERY processed links (default 5) and once more when the run finishes. The checkpoint stores the set of already-processed link IDs.
If the script is interrupted — network error, rate limit, Ctrl-C — simply re-run it:
uv run autotag.pyIt loads progress.json on startup, skips links that are already done, and continues from where it left off. To start fresh, delete progress.json.
None.
This is “itch-and-scratch-ware”. I had an itch, so I scratched it. The end. I may continue to improve this as I need to, but if you want any real changes, feel free to fork it and adapt it for your own needs.