SLOPSHOPPER

connection-badge

Shows in the prompt footer whether the session runs locally or over SSH, and on which server

newspinnercommandprocess
v0.1.1no licenseupdated 2026-10-08filiptaka/connection-badge
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · connection-badge
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /where ⎿ connection-badge: Connection: local (this computer) ⎿ connection-badge: Machine: dev ⎿ connection-badge: User: dev ⎿ connection-badge: Directory: /work/app ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

connection-badge

A Claude Code mod that shows, in the footer under the prompt, whether the session runs on your own computer or on a server over SSH, and which server.

SessionFooter label
Locallocal: DESKTOP-ABC123
SSHssh: deploy@web-01 (10.0.0.5)
WSLwsl: Ubuntu

/where prints the details: connection type, machine name, address and port, user, the address the connection comes from, and the working directory.

It draws in the terminal and in the Claude desktop app's Code tab. Mods are an early-access Claude Code feature; this one is built and tested on Claude Code 2.1.293.

Install

In Claude Code in a terminal:

/plugin install connection-badge --marketplace filiptaka/connection-badge

Answer y to add the marketplace, then pick a scope.

Or from a shell:

claude plugin marketplace add filiptaka/connection-badge
claude plugin install connection-badge@connection-badge

SSH sessions from the desktop app

When the desktop app opens an SSH session, Claude Code runs on the server, so install the mod on each server too (the shell commands above, run there).

From a folder instead

Clone the repo and point Claude Code at the folder in ~/.claude/settings.json:

{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/connection-badge" } }

How it decides

  • SSH: SSH_CONNECTION, SSH_CLIENT or SSH_TTY is set, the same check Claude Code makes, or Claude Code runs from the copy the desktop app installs under ~/.claude/remote/.
  • WSL: WSL_DISTRO_NAME is set, or the kernel release names Microsoft.
  • Local: anything else.

The server's name is its own hostname, and its address comes from SSH_CONNECTION.

Development

claude plugin validate .
claude plugin test .
Source 2 files
hooks/register.tsx 172 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Connection } from '../types'
5
6const connection = atom({ plugin: 'connection-badge', key: 'connection' } as const, null)
7
8const KINDS = {
9  local: 'local (this computer)',
10  ssh: 'SSH (remote server)',
11  wsl: 'WSL (Linux on this computer)',
12} as const
13
14export const register: Register = on => {
15  on('session.start', async ($, e, next) => {
16    await $.command.register({
17      name: 'where',
18      description: 'Show whether this session runs locally or over SSH, and on which server',
19    })
20    const found = await detect($)
21    await update($, connection, () => found)
22
23    return next(e)
24  })
25
26  // The footer under the prompt. The terminal draws the mode labels it is
27  // handed; the desktop app draws only a tree a hook returns.
28  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
29    const found = await read($, connection)
30
31    if (found === null) {
32      return next(e)
33    }
34
35    const modes = [...e.props.modes, badge(found)]
36
37    if (e.surface === 'desktop') {
38      const { Text } = $.ui.resolve(e)
39
40      return <Text>{modes.join(' & ')}</Text>
41    }
42
43    return next({ ...e, props: { ...e.props, modes } })
44  })
45
46  on('command.run', { command: 'where' }, async $ => {
47    const found = (await read($, connection)) ?? (await detect($))
48
49    return { text: describe(found, await $.session.cwd()) }
50  })
51}
52
53async function detect($: EngineInterface): Promise<Connection> {
54  const [sshConnection, sshClient, sshTty, wslDistro, execPath, user, logname, username] =
55    await Promise.all([
56      $.env.get('SSH_CONNECTION'),
57      $.env.get('SSH_CLIENT'),
58      $.env.get('SSH_TTY'),
59      $.env.get('WSL_DISTRO_NAME'),
60      $.env.get('CLAUDE_CODE_EXECPATH'),
61      $.env.get('USER'),
62      $.env.get('LOGNAME'),
63      $.env.get('USERNAME'),
64    ])
65  const base = { host: await hostname($), user: user || logname || username }
66
67  // What Claude Code itself checks to tell it runs over SSH.
68  if (sshConnection || sshClient || sshTty) {
69    return clean({ kind: 'ssh', ...base, ...parseSsh(sshConnection, sshClient) })
70  }
71
72  if (wslDistro) {
73    return clean({ kind: 'wsl', ...base, distro: wslDistro })
74  }
75
76  // The desktop app runs a remote session from the copy of Claude Code it
77  // installs under ~/.claude/remote on that machine (a server or a WSL distro).
78  if (execPath !== undefined && /[\\/]\.claude[\\/]remote[\\/]/.test(execPath)) {
79    return clean({ kind: (await isWsl($)) ? 'wsl' : 'ssh', ...base })
80  }
81
82  return clean({ kind: 'local', ...base })
83}
84
85async function hostname($: EngineInterface): Promise<string | undefined> {
86  const windows = await $.env.get('COMPUTERNAME')
87
88  if (windows) {
89    return windows
90  }
91
92  const linux = await $.fs.read('/proc/sys/kernel/hostname').catch(() => '')
93
94  if (linux.trim() !== '') {
95    return linux.trim()
96  }
97
98  const ran = await $.process.run(['hostname'], { timeoutMs: 3000 }).catch(() => undefined)
99
100  return ran?.exitCode === 0 && ran.stdout.trim() !== '' ? ran.stdout.trim() : undefined
101}
102
103async function isWsl($: EngineInterface): Promise<boolean> {
104  const release = await $.fs.read('/proc/sys/kernel/osrelease').catch(() => '')
105
106  return /microsoft|wsl/i.test(release)
107}
108
109function parseSsh(connection: string | undefined, client: string | undefined) {
110  // SSH_CONNECTION is "<client ip> <client port> <server ip> <server port>"
111  const full = connection?.trim().split(/\s+/) ?? []
112
113  if (full.length >= 4) {
114    return { clientIp: full[0], serverIp: full[2], serverPort: full[3] }
115  }
116
117  // SSH_CLIENT is "<client ip> <client port> <server port>"
118  const short = client?.trim().split(/\s+/) ?? []
119
120  if (short.length >= 3) {
121    return { clientIp: short[0], serverPort: short[2] }
122  }
123
124  return {}
125}
126
127// $.state holds JSON data: leave out the fields nothing was found for.
128function clean(found: Connection): Connection {
129  return Object.fromEntries(
130    Object.entries(found).filter(([, value]) => value !== undefined && value !== ''),
131  ) as Connection
132}
133
134function badge(found: Connection): string {
135  if (found.kind === 'ssh') {
136    const user = found.user === undefined ? '' : `${found.user}@`
137    const ip = found.host !== undefined && found.serverIp !== undefined ? ` (${found.serverIp})` : ''
138
139    return `ssh: ${user}${found.host ?? found.serverIp ?? 'remote server'}${ip}`
140  }
141
142  if (found.kind === 'wsl') {
143    return `wsl: ${found.distro ?? found.host ?? 'Linux'}`
144  }
145
146  return found.host === undefined ? 'local' : `local: ${found.host}`
147}
148
149function describe(found: Connection, cwd: string): string {
150  const port = found.serverPort === undefined ? '' : `:${found.serverPort}`
151  const address =
152    found.serverIp === undefined
153      ? undefined
154      : found.serverIp.includes(':') && port !== ''
155        ? `[${found.serverIp}]${port}`
156        : `${found.serverIp}${port}`
157  const rows: [string, string | undefined][] = [
158    ['Connection', KINDS[found.kind]],
159    ['Machine', found.host],
160    ['Address', address],
161    ['User', found.user],
162    ['WSL distro', found.distro],
163    ['Connected from', found.clientIp],
164    ['Directory', cwd],
165  ]
166
167  return rows
168    .filter((row): row is [string, string] => row[1] !== undefined)
169    .map(([label, value]) => `${label}: ${value}`)
170    .join('\n')
171}
172
types/index.d.ts 21 lines
1export type ConnectionKind = 'local' | 'ssh' | 'wsl'
2
3export type Connection = {
4  kind: ConnectionKind
5  /** The machine Claude Code runs on: the server, for an SSH session. */
6  host?: string
7  user?: string
8  /** From SSH_CONNECTION: the address and port the SSH client connected to. */
9  serverIp?: string
10  serverPort?: string
11  /** From SSH_CONNECTION: the address the connection came from. */
12  clientIp?: string
13  distro?: string
14}
15
16declare module 'claude-code' {
17  interface PluginState {
18    'connection-badge': { connection: Connection | null }
19  }
20}
21