SLOPSHOPPER

file-links

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…

newpaneguardcommandprocess
v0.2.0MITupdated 2026-10-16ecombuilder-web/TripleDigit-file-links
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · file-links
│ ┃ Files ✕ › fix the failing auth test and add an audit log call │ ┃ Search files by name │ ┃ [ cache.ts ] ⏺ Read(src/auth.ts) │ ┃ \work\app\src ⎿ Read 6 lines │ ┃ [ audit.ts ] ⏺ Update(src/auth.ts) │ ┃ \work\app\src ⎿ Added 2 lines, removed 1 line │ ┃ [ auth.ts ] ⏺ Bash(bun test) │ ┃ \work\app\src ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /file-links │ ⎿ file-links: Files pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Files
Search files by name [ cache.ts ] \work\app\src [ audit.ts ] \work\app\src [ auth.ts ] \work\app\src
README

<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>

📁 File Links

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 → &nbsp;·&nbsp; Custom panels &nbsp;·&nbsp; Install &nbsp;·&nbsp; 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>


Built by Triple Digit

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:

  • 🤖 Custom AI agents that handle real work in your business: support, research, operations, reporting.
  • ⚙️ Workflow automation across the tools your team already uses: email, CRM, docs, spreadsheets, internal systems.
  • 🧠 Agentic coding setups like this one: Claude Code, MCP servers, mods and skills tuned to your codebase and team.
  • 🌐 AI-powered websites and internal tools, designed, built, shipped and maintained.

If your team is wondering where AI could save hours every week, that is the conversation we like to have.

A panel built for your team

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:

  • 📊 Live business dashboards next to the chat: sales, orders, support queue, campaign results.
  • ✅ Approval queues: drafts, refunds or purchases that an AI agent has prepared and that wait for one click from you.
  • 🤖 Agent control rooms: which automations are running, which failed and which need a human.
  • 🔌 Panels wired to your systems: CRM, inbox, spreadsheets, internal tools, through MCP connectors we set up and secure.
  • 🎨 Your brand, your rules: your colours, your wording, your team's permissions.

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


What it shows

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:

  • Write, Edit and NotebookEdit tool calls.
  • Bash commands, such as FFmpeg renders and yt-dlp downloads. After each command it adds files that changed while the command ran, found in the files and folders the command names, in the current folder, and one level of subfolders under each.

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.

Install

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.

How it works

PieceRole
hooks/register.tsxThe 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.tsTests 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.tsState contract

Privacy and safety

  • Everything stays on your machine. The mod reads file names and modified times in local folders. It makes no network requests and sends nothing anywhere.
  • It never reads file contents and never changes or deletes files.
  • The only thing it launches is explorer.exe /select,<path> for a file you click.

Develop

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.

License

MIT, © 2026 Triple Digit. Free to use, change and share.


Want AI built around how your company works?

Triple Digit designs and builds tailored AI agents, automations and tools for each client.

triple-digit.com →

<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>

Source 2 files
hooks/register.tsx 153 lines
1import { 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}
153
types/index.d.ts 8 lines
1declare module 'claude-code' {
2  interface PluginState {
3    'file-links': { files: string[]; query: string }
4  }
5}
6
7export type FileLinksFiles = string[]
8