cclinks
English | 日本語
Open links from your Claude Code sessions with the keyboard, without touching the mouse.
Claude Code renders [label](url) as the label only. The URL never reaches the terminal
buffer, so tools that scrape the screen — tmux-fzf-url, your terminal's own link detector,
VS Code's Open Detected Link — cannot find it. There is nothing on screen to find.
cclinks reads the session transcripts instead, where the URLs are still intact — across
every project and every tab, with each row saying which session it came from.
The Sources: list behind the popup is what Claude Code actually printed: twenty labels,
and not one URL among them. The picker has every one of those, plus the links from two
other projects — and each row says which session it came from. One query, example, pulls
matches out of two of them at once.
Pick with the arrow keys or by typing, press Enter, and the link opens in your browser.
Why not just scrape the screen, or click the link?
| Screen scraping | Ctrl/⌘-click | cclinks | |
|---|---|---|---|
| Bare URLs | Yes | Yes | Yes |
| Markdown links | No | Yes¹ | Yes |
| Scrolled out of view | No | No | Yes |
| Label available for search | No | No | Yes |
| Without leaving the keyboard | Yes | No | Yes |
¹ In terminals that render OSC 8 hyperlinks — iTerm2, WezTerm, Ghostty, the VS Code and Cursor integrated terminals. The URL is still absent from the buffer; the terminal holds it out of band.
Clicking works, and it is the reason this tool is not about reaching the link. It is about not moving your hand to reach it. If your hands are on the keyboard while Claude is talking, they stay there.
Screen scrapers are still useful for file paths and for output from other programs. The two complement each other; bind them to neighbouring keys.
Install
Requires Python 3.10+ and fzf.
uv tool install cclinks
Or with pipx:
pipx install cclinks
For the latest commit rather than the latest release, install from the repository
instead: uv tool install git+https://github.com/takkuhiro/cclinks.
Usage
cclinks # pick a link and open it
cclinks --print # list links, open nothing
cclinks --all # every session ever recorded, not just the recent ones
cclinks --since 30d # widen the window (12h, 7d, 2w, or all)
cclinks --limit 50 # read more sessions (0 for no cap)
cclinks --scope project # only the sessions for this directory
cclinks --scope session # only one session
cclinks --active # only the session you last typed into (needs a hook, below)
cclinks --latest # only the most recently updated session
cclinks --cwd PATH # target a specific directory
cclinks --no-color # do not color the picker
By default it gathers the 20 most recent sessions from the last 7 days, across every project. Sessions are listed newest first, and links keep the order they appeared in.
A URL is listed once even if several sessions mentioned it, attributed to the newest one. When the same URL appears both as a bare URL and as a Markdown link, the label is kept.
Where each link came from
Once more than one session is on screen, every row is led by the session it came from:
cclinks/Origin column for the picker │ the fzf manual ⟶ https://github.com/junegunn/fzf
techblogs/Draft on transformer memory │ the original PDF ⟶ https://arxiv.org/abs/...
The name is the project directory, then the title the session gave itself (falling back to what you last asked it, and then to a short session id). It is searchable like everything else on the row, so typing a project name narrows the list to that project.
The column is dropped entirely when every link comes from the same session — there is
nothing to tell apart — which is what --scope session, --latest and --active give you.
Colors
The picker colors the label, the URL and the origin differently so the list reads by label. Override with raw SGR parameters, or turn it off:
CCLINKS_LABEL_COLOR=35 cclinks # magenta labels (default: 36, cyan)
CCLINKS_URL_COLOR=32 cclinks # green URLs (default: 90, grey)
CCLINKS_ORIGIN_COLOR=90 cclinks # grey origins (default: 35, magenta)
CCLINKS_ORIGIN_WIDTH=48 cclinks # a wider origin column (default: 32 columns)
cclinks --no-color
Japanese titles cost two columns per character, so CCLINKS_ORIGIN_WIDTH is worth raising
on a wide terminal.
--print is always plain, so it stays usable in a pipe.
Which sessions does it read?
All of them, within a window: the 20 most recent sessions touched in the last 7 days.
--limit and --since move those two numbers, --all removes both, and --scope
narrows by project instead of by time:
| reads | |
|---|---|
| (default) | every project, newest 20 sessions of the last 7 days |
--all |
every session of every project, however old |
--scope project |
every session for --cwd, same window |
--scope session |
one session: the newest for --cwd, or the newest anywhere if that directory has none |
--latest |
one session: the most recently updated, whatever the directory |
--active |
one session: the one you last typed into |
The window exists because a picker of every link you have ever been shown is not a picker.
When nothing turns up, the empty picker says how far back it looked, so widening with
--all is the obvious next move.
Reading one session
The single-session modes are for when you want this tab and nothing else. Picking the
right one is harder than it sounds: a picker started from a hotkey is a sibling of Claude
Code, not a child, so CLAUDE_CODE_SESSION_ID never reaches it, and several sessions
routinely share a working directory. Modification time is a poor tiebreak too — a tab
working through a long task writes continuously, so it wins on mtime while you are reading
a different one.
--active uses the session you last typed into, which is a much closer match for the tab
in front of you. The session has to announce itself, which is what the hook below does.
The hook
contrib/user-prompt-submit-hook.sh records the session on every prompt you submit. It
needs jq. Copy it somewhere, chmod +x it, then add this
to ~/.claude/settings.json:
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash /path/to/user-prompt-submit-hook.sh",
"timeout": 5
}
]
}
]
}
It writes ~/.claude/cclinks-active.json — the session id, its transcript path, and its
working directory. Not the prompt itself. Override the location with
CCLINKS_ACTIVE_FILE.
Without the hook, --active behaves exactly like --latest, so you can put it in a
keybinding first and set the hook up afterwards.
Binding it to a key
Claude Code occupies the terminal, so the picker has to run somewhere else. Pick whichever fits your setup.
The examples below use the default, which gathers recent sessions and labels each row with
its origin. Add --active once the hook is in place if you would rather see only the tab in
front of you.
tmux
Needs tmux 3.2 or newer for display-popup.
bind-key u display-popup -E -w 80% -h 60% "cclinks"
The popup does not source your shell rc, so it uses the PATH the tmux server started with. If the popup flashes and vanishes, use an absolute path instead.
Shell alias
alias ccl='cclinks'
VS Code / Cursor task
Two files, both in the user directory rather than a workspace, so the key also works in a window with no folder open.
First the task, in ~/Library/Application Support/Cursor/User/tasks.json (for VS Code,
Code in place of Cursor).
{
"version": "2.0.0",
"tasks": [
{
"label": "cclinks",
"type": "shell",
"command": "${userHome}/.local/bin/cclinks",
"options": {
"env": { "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${env:PATH}" }
},
"presentation": {
"echo": false, "reveal": "always", "focus": true,
"panel": "dedicated", "close": true, "clear": true, "showReuseMessage": false
},
"problemMatcher": []
}
]
}
The task shell does not inherit your interactive PATH, which is why the command and the
PATH entry are spelled out.
Then the key, in keybindings.json beside it:
{
"key": "alt+cmd+q",
"command": "workbench.action.tasks.runTask",
"args": "cclinks",
"when": "terminalFocus"
}
terminalFocus keeps the binding to the terminal, so the key stays free while you are
editing. Drop it to reach the picker from anywhere in the window.
Raycast
Drop a script command in your Raycast script directory and give it a hotkey:
#!/bin/bash
# @raycast.schemaVersion 1
# @raycast.title cclinks
# @raycast.mode silent
open -a Terminal "$HOME/.local/bin/cclinks"
Development
uv sync
uv run pytest
Limitations
- Only Claude Code transcripts (
~/.claude/projects/**/*.jsonl) are supported. - Session titles come from a field Claude Code writes for its own use, which is not a documented format. If it ever changes, rows fall back to the last prompt and then to a short session id; nothing else breaks.
- The transcript is written when a response completes, so a link is pickable only after Claude finishes speaking.
open/xdg-openare used to launch the browser. Tested on macOS with tmux 3.6.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cclinks-0.2.0.tar.gz.
File metadata
- Download URL: cclinks-0.2.0.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1879d4d63a886a56cee89a95f209ce8380fcf158e25b09bc49ad46b42e112057
|
|
| MD5 |
e7e5a5f446b54f1d5fc8997a10fb87c8
|
|
| BLAKE2b-256 |
93d77513971ae7d602d28b3efc7d8d871e3f1b037a31b8541575c86fb33b76ab
|
Provenance
The following attestation bundles were made for cclinks-0.2.0.tar.gz:
Publisher:
publish.yml on takkuhiro/cclinks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cclinks-0.2.0.tar.gz -
Subject digest:
1879d4d63a886a56cee89a95f209ce8380fcf158e25b09bc49ad46b42e112057 - Sigstore transparency entry: 2500678262
- Sigstore integration time:
-
Permalink:
takkuhiro/cclinks@1f9cad1ff34f6455af80741836f0b48908df76fe -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/takkuhiro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1f9cad1ff34f6455af80741836f0b48908df76fe -
Trigger Event:
push
-
Statement type:
File details
Details for the file cclinks-0.2.0-py3-none-any.whl.
File metadata
- Download URL: cclinks-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e94c2b97a312ed6bd754aabc27ef2d9574dbb632204defb76ca24207acb6bd58
|
|
| MD5 |
dbc354e1511fe80526cce9f9f089af64
|
|
| BLAKE2b-256 |
9bc2b562370257d6606b952d2d2f3c7d069aa8312d2f5c1c0f75d1cc412462b6
|
Provenance
The following attestation bundles were made for cclinks-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on takkuhiro/cclinks
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cclinks-0.2.0-py3-none-any.whl -
Subject digest:
e94c2b97a312ed6bd754aabc27ef2d9574dbb632204defb76ca24207acb6bd58 - Sigstore transparency entry: 2500678279
- Sigstore integration time:
-
Permalink:
takkuhiro/cclinks@1f9cad1ff34f6455af80741836f0b48908df76fe -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/takkuhiro
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1f9cad1ff34f6455af80741836f0b48908df76fe -
Trigger Event:
push
-
Statement type: