SLOPSHOPPER

where-am-i

Shows the session's directory and git branch / worktree at the bottom right of the prompt

newspinnerguardstatusprocesstimer
A shopper browsing a rack in a slop shop
README

where-am-i

日本語

A Claude Code mod that always shows where the session is: the current directory and the git branch / worktree, at the bottom right of the prompt.

~/workspace/my-app main                          main is green
~/workspace/app-wt/fix-x fix-x fix/x             fix-x (worktree) is magenta, fix/x (branch) is green
focus · ~/workspace/my-app main                  the engine's mode names stay at the front

Requirements

Claude Code with mods support. Mods are early access; this was built and tested on Claude Code 2.1.289. Behavior on other versions is not guaranteed. git must be on your PATH.

Install

claude plugin marketplace add generonishimura/ccm-where-am-i
claude plugin install where-am-i@ccm-where-am-i

Then run /reload-plugins in your session, or restart Claude Code.

How it works

  • It runs git rev-parse --path-format=absolute --git-dir --git-common-dir --show-toplevel --abbrev-ref HEAD in the session's directory.
  • If --git-dir and --git-common-dir differ, the directory is a linked worktree, and the worktree is named after its folder.
  • A detached HEAD is shown as detached@<sha>. Outside a git repository, only the directory is shown.
  • It refreshes on session start, after every tool call, when a turn completes, and every 5 seconds (to catch branch changes made in another terminal or IDE).
  • The display only updates when the value changes, so the footer does not redraw on every poll.

Customizing

Edit the constants at the top of where-am-i/hooks/register.tsx:

const POLL_MS = 5000
const BRANCH_COLOR = 'green'
const WORKTREE_COLOR = 'magenta'

Development

Clone the repo and add it as a directory marketplace, so edits are picked up live (run /reload-plugins after editing):

claude plugin marketplace add /path/to/ccm-where-am-i
claude plugin install where-am-i@ccm-where-am-i

Run the tests, validate the manifests and type check:

claude plugin test where-am-i
claude plugin validate .
claude plugin validate where-am-i
tsc -p where-am-i

where-am-i/.claude-plugin/types/ is generated by Claude Code when the mod is loaded (it is git-ignored). Load the mod once before running tsc.

Similar projects

choas/claude-mods has cwd-status and git-status mods that show the directory and branch (with uncommitted / unpushed counts) in the same footer.

License

MIT

Source 2 files
hooks/register.tsx 118 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Where } from '../types'
5
6const POLL_MS = 5000
7const BRANCH_COLOR = 'green'
8const WORKTREE_COLOR = 'magenta'
9
10const where = atom({ plugin: 'where-am-i', key: 'where' } as const, null)
11
12// /Users/<name>/... and /home/<name>/... read as ~/...
13export const tildify = (path: string): string =>
14  path.replace(/^\/(Users|home)\/[^/]+(?=\/|$)/, '~')
15
16const basename = (path: string): string =>
17  path.replace(/\/+$/, '').split('/').pop() ?? path
18
19// Lines of `git rev-parse --path-format=absolute --git-dir --git-common-dir
20// --show-toplevel --abbrev-ref HEAD`; a linked worktree's git dir differs
21// from the common one.
22export const parseRevParse = (cwd: string, stdout: string): Where => {
23  const [gitDir, commonDir, top, ref] = stdout.trim().split('\n')
24  if (gitDir === undefined || commonDir === undefined || top === undefined || ref === undefined) {
25    return { cwd }
26  }
27  return {
28    cwd,
29    branch: ref,
30    worktree: gitDir === commonDir ? undefined : basename(top),
31  }
32}
33
34export type Segment = { text: string; color?: string }
35
36// What the footer shows, left to right: the engine's modes, the directory
37// (both dim), then the worktree and the branch in their colors.
38export const segments = (here: Where, modes: readonly string[]): Segment[] => [
39  ...(modes.length === 0 ? [] : [{ text: `${modes.join(' & ')} · ` }]),
40  { text: tildify(here.cwd) },
41  ...(here.worktree === undefined ? [] : [{ text: ` ${here.worktree}`, color: WORKTREE_COLOR }]),
42  ...(here.branch === undefined ? [] : [{ text: ` ${here.branch}`, color: BRANCH_COLOR }]),
43]
44
45const isSame = (a: Where | null, b: Where): boolean =>
46  a !== null && a.cwd === b.cwd && a.branch === b.branch && a.worktree === b.worktree
47
48async function locate($: EngineInterface): Promise<Where> {
49  const cwd = await $.session.cwd()
50  const run = (argv: string[]) => $.process.run(['git', ...argv], { cwd, timeoutMs: 3000 })
51
52  const found = await run([
53    'rev-parse', '--path-format=absolute',
54    '--git-dir', '--git-common-dir', '--show-toplevel', '--abbrev-ref', 'HEAD',
55  ]).catch(() => undefined)
56  if (found === undefined || found.exitCode !== 0) return { cwd }
57
58  const located = parseRevParse(cwd, found.stdout)
59  if (located.branch !== 'HEAD') return located
60
61  const sha = await run(['rev-parse', '--short', 'HEAD']).catch(() => undefined)
62  return { ...located, branch: sha?.exitCode === 0 ? `detached@${sha.stdout.trim()}` : 'detached' }
63}
64
65let isRefreshing = false
66
67async function refresh($: EngineInterface): Promise<void> {
68  if (isRefreshing) return
69  isRefreshing = true
70  try {
71    const next = await locate($)
72    // Writing only on a change keeps the footer from redrawing every poll.
73    if (!isSame(await read($, where), next)) await update($, where, () => next)
74  } finally {
75    isRefreshing = false
76  }
77}
78
79export const register: Register = on => {
80  on('session.start', async ($, e, next) => {
81    const started = await next(e)
82    // 0.1.0 pinned a status line; take it down.
83    $.ui.status(undefined)
84    void refresh($)
85    // Catches a checkout made outside this session (another terminal, an IDE).
86    $.clock.every(POLL_MS, () => void refresh($))
87    return started
88  })
89
90  // A Bash `git switch`, EnterWorktree / ExitWorktree, /cd: show it right away.
91  on('tool.call', async ($, e, next) => {
92    const result = await next(e)
93    void refresh($)
94    return result
95  })
96
97  on('turn.complete', async ($, e, next) => {
98    void refresh($)
99    return next(e)
100  })
101
102  // The dim labels at the right of the prompt footer: the engine's modes
103  // stay, the location follows them.
104  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
105    const here = await read($, where)
106    if (here === null) return next(e)
107
108    const { Box, Text } = $.ui.resolve(e)
109    const parts = segments(here, e.props.modes).map(part =>
110      part.color === undefined
111        ? <Text dimColor>{part.text}</Text>
112        : <Text color={part.color}>{part.text}</Text>,
113    )
114
115    return <Box flexDirection="row">{parts}</Box>
116  })
117}
118
types/index.d.ts 12 lines
1export type Where = {
2  cwd: string
3  branch?: string
4  worktree?: string
5}
6
7declare module 'claude-code' {
8  interface PluginState {
9    'where-am-i': { where: Where | null }
10  }
11}
12