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

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.
| Session | Footer label |
|---|---|
| Local | local: DESKTOP-ABC123 |
| SSH | ssh: deploy@web-01 (10.0.0.5) |
| WSL | wsl: 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.
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
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).
Clone the repo and point Claude Code at the folder in ~/.claude/settings.json:
{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/connection-badge" } }
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_DISTRO_NAME is set, or the kernel release names Microsoft.The server's name is its own hostname, and its address comes from SSH_CONNECTION.
claude plugin validate .
claude plugin test .hooks/register.tsx 172 lines1import { 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}
172types/index.d.ts 21 lines1export 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