Files pane by Triple Digit (triple-digit.com, tailored AI solutions for each company): every file Claude creates or edits in a session, one click from Windows…

<a href="https://triple-digit.com"><img src="https://raw.githubusercontent.com/ecombuilder-web/TripleDigit-crab-agents/main/docs/triple-digit-logo.jpg" alt="Triple Digit" width="360"></a>
Every file Claude makes, one click from File Explorer.
A free Claude Code mod by Triple Digit, the AI agency that builds tailored AI solutions for each company.
Work with Triple Digit → · Custom panels · Install · How it works
<img src="docs/file-links-pane.png" alt="The File Links pane: a search field at the top and a list of files, each a button with its folder path underneath" width="380">
<sub>The Files pane. Each file is a button that opens File Explorer with that file selected.</sub>
We build AI systems for clients all day, and every session leaves files behind: documents, scripts, exports, FFmpeg renders, downloads. Hunting for them in folders wastes time. File Links lists them in one place beside the chat, so we made it and are sharing it.
Triple Digit is an AI agency. We don't sell one product to everyone. We look at how your company actually works, then design and build the AI that fits it:
If your team is wondering where AI could save hours every week, that is the conversation we like to have.
File Links is one example of what a custom Claude Desktop panel can do. Claude Desktop lets a mod draw its own live panel beside your conversation, so the information your team needs is always one glance away, inside the tool they already work in.
Triple Digit builds custom panels and mods for your company. Each one is designed around your own workflow and data, for example:
The panel stays on your machine and reads only what you connect to it. We build it, test it, hand it over and keep it working as your tools change.
👉 Tell us what your team looks at all day at triple-digit.com
👉 Book a call at triple-digit.com
File Links adds a Files pane beside the chat. Every file the session creates or changes is listed, newest first, with its folder underneath. Click a file and Windows File Explorer opens with that file selected.
When the list gets long, type in the search field at the top of the pane. It filters as you type, ignores upper and lower case, and matches the file name or its folder path. Clear the field to see everything again.
It picks files up from:
Limits: up to 30 files per Bash command, up to 200 in the list, and no more than one subfolder level. The list lives in memory, so it starts empty in each new session. Unrelated files changed in a scanned folder while a command runs can also appear. Type /file-links to reopen the pane.
Windows only for now: the click uses explorer.exe.
You need Claude Code 2.1.295 or newer.
Option A: install it as a plugin. In a Claude Code session:
/plugin install file-links --marketplace ecombuilder-web/TripleDigit-file-links
Choose the user scope so it loads in every new session, desktop app sessions included.
Option B: run it from a clone.
git clone https://github.com/ecombuilder-web/TripleDigit-file-links.git ~/.claude/mods/file-links
Then add the folder to the env block of ~/.claude/settings.json. Separate folders with ; on Windows:
"env": { "CLAUDE_CODE_PLUGIN_DIRS": "C:\\Users\\<you>\\.claude\\mods\\file-links" }
The pane opens by itself in every new session. A session started before the install needs a restart.
| Piece | Role |
|---|---|
hooks/register.tsx | The hooks module: opens the pane, records files from tool calls, scans after Bash commands, draws the search field and the list, and opens Explorer on click |
hooks/register.test.ts | Tests for the pane: a written file is listed, an empty pane says so, a denied write is not listed, the search field filters the list |
types/index.d.ts | State contract |
explorer.exe /select,<path> for a file you click.Run both from the folder that holds the clone (here ~/.claude/mods/file-links):
claude plugin validate ~/.claude/mods/file-links
claude plugin test ~/.claude/mods/file-links
validate checks the manifest, the hooks module and the state contract. test runs hooks/register.test.ts against the engine and covers the pane and its search field, not the Bash scan or the Explorer click.
MIT, © 2026 Triple Digit. Free to use, change and share.
Triple Digit designs and builds tailored AI agents, automations and tools for each client.
<sub>File Links is an independent community mod. It is not made or endorsed by Anthropic. Claude and Claude Code are trademarks of Anthropic.</sub>
hooks/register.tsx 153 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4const PANE = 'file-links'
5const files = atom({ plugin: 'file-links', key: 'files' } as const, [] as string[])
6const query = atom({ plugin: 'file-links', key: 'query' } as const, '')
7
8const toWindows = (path: string) =>
9 path.replace(/^\/([a-zA-Z])\//, '$1:/').replace(/\//g, '\\')
10const split = (path: string) => {
11 const cut = Math.max(path.lastIndexOf('\\'), path.lastIndexOf('/'))
12 return { name: path.slice(cut + 1), dir: path.slice(0, Math.max(cut, 0)) }
13}
14
15const TOKEN = /(?:"([^"]+)"|'([^']+)'|([^\s"'|&;<>()]+))/g
16const dirOf = (p: string) => {
17 const cut = Math.max(p.lastIndexOf('\\'), p.lastIndexOf('/'))
18 return cut > 0 ? p.slice(0, cut) : undefined
19}
20const candidates = (command: string) => {
21 const out = new Set<string>()
22 for (const m of command.matchAll(TOKEN)) {
23 const t = m[1] ?? m[2] ?? m[3]
24 if (t && /\.[A-Za-z0-9]{1,5}$/.test(t) && !t.startsWith('-') && !/^https?:/.test(t)) out.add(t)
25 }
26 return [...out]
27}
28
29const absolute = async ($: any, path: string) => {
30 if (/^([a-zA-Z]:|\/|\\)/.test(path)) return path
31 const stat = await $.fs.stat(path, { resolve: true }).catch(() => undefined)
32 return stat?.realPath ?? path
33}
34
35const track = async ($: any, path: string | undefined) => {
36 if (!path) return
37 const full = toWindows(path)
38 await update($, files, list => [full, ...list.filter(one => one !== full)].slice(0, 200))
39}
40
41export const register: Register = on => {
42 on('session.start', async ($, e, next) => {
43 await $.command.register({ name: 'file-links', description: 'Show files made this session, with links to File Explorer' })
44 void $.ui.open({ id: PANE, title: 'Files' })
45 return next(e)
46 })
47
48 on('command.run', { command: 'file-links' }, async $ => {
49 await $.ui.open({ id: PANE, title: 'Files' })
50 return { text: 'Files pane opened.' }
51 })
52
53 on('tool.call', { tool: 'Write' }, async ($, e, next) => {
54 const ran = await next(e)
55 if (ran.deny === undefined && ran.isError !== true) await track($, e.file_path)
56 return ran
57 }).catch(($, e, next) => next(e))
58
59 on('tool.call', { tool: 'Edit' }, async ($, e, next) => {
60 const ran = await next(e)
61 if (ran.deny === undefined && ran.isError !== true) await track($, e.file_path)
62 return ran
63 }).catch(($, e, next) => next(e))
64
65 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
66 const t0 = (await $.clock.now()) - 1000
67 const tokens = candidates(e.command)
68 const ran = await next(e)
69 if (ran.deny !== undefined) return ran
70
71 const found = new Set<string>()
72 const dirs = new Set<string>([''])
73 for (const tok of tokens) {
74 const stat = await $.fs.stat(tok).catch(() => undefined)
75 if (stat?.kind === 'file' && stat.mtimeMs >= t0) found.add(tok)
76 const d = dirOf(tok)
77 if (d) dirs.add(d)
78 }
79 for (const m of e.command.matchAll(TOKEN)) {
80 const t = m[1] ?? m[2] ?? m[3]
81 if (!t || t.startsWith('-') || /^https?:/.test(t) || /[*?%]/.test(t)) continue
82 const stat = await $.fs.stat(t).catch(() => undefined)
83 if (stat?.kind === 'dir') dirs.add(t)
84 }
85 for (const d of [...dirs].slice(0, 10)) {
86 const top = await $.fs.list(d || undefined).catch(() => [])
87 for (const sub of top.filter(x => x.kind === 'dir').slice(0, 40)) {
88 dirs.add(d ? `${d}/${sub.name}` : sub.name)
89 }
90 }
91 for (const d of dirs) {
92 const entries = await $.fs.list(d || undefined).catch(() => [])
93 for (const f of entries) {
94 if (f.kind === 'file' && f.mtimeMs >= t0) found.add(d ? `${d}/${f.name}` : f.name)
95 }
96 }
97 for (const path of [...found].slice(0, 30)) await track($, await absolute($, path))
98 return ran
99 }).catch(($, e, next) => next(e))
100
101 on('tool.call', { tool: 'NotebookEdit' }, async ($, e, next) => {
102 const ran = await next(e)
103 if (ran.deny === undefined && ran.isError !== true) await track($, (e as any).notebook_path)
104 return ran
105 }).catch(($, e, next) => next(e))
106
107 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
108 const els = $.ui.resolve(e)
109 const { Box, Text, Button } = els
110 const Input = (els as any).Input as ((props: any) => any) | undefined
111 const all = await read($, files)
112 const q = await read($, query)
113 const needle = q.trim().toLowerCase()
114 const list = needle ? all.filter(path => path.toLowerCase().includes(needle)) : all
115
116 return (
117 <Box flexDirection="column">
118 {Input && (
119 <Input
120 key="search"
121 placeholder="Search files by name"
122 value={q}
123 onInput={(value: string) => {
124 void update($, query, () => value)
125 }}
126 onSubmit={(value: string) => {
127 void update($, query, () => value)
128 }}
129 />
130 )}
131 {all.length === 0 && <Text dimColor>No files yet this session.</Text>}
132 {all.length > 0 && list.length === 0 && <Text dimColor>No file matches "{q}".</Text>}
133 {list.map(path => {
134 const { name, dir } = split(path)
135 return (
136 <Box key={path} flexDirection="column">
137 <Button
138 key={path}
139 onPress={() => {
140 void $.process.run(['explorer.exe', `/select,${path}`])
141 }}
142 >
143 {name}
144 </Button>
145 <Text dimColor>{dir}</Text>
146 </Box>
147 )
148 })}
149 </Box>
150 )
151 })
152}
153types/index.d.ts 8 lines1declare module 'claude-code' {
2 interface PluginState {
3 'file-links': { files: string[]; query: string }
4 }
5}
6
7export type FileLinksFiles = string[]
8