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

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
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.
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.
git rev-parse --path-format=absolute --git-dir --git-common-dir --show-toplevel --abbrev-ref HEAD in the session's directory.--git-dir and --git-common-dir differ, the directory is a linked worktree, and the worktree is named after its folder.detached@<sha>. Outside a git repository, only the directory is shown.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'
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.
choas/claude-mods has cwd-status and git-status mods that show the directory and branch (with uncommitted / unpushed counts) in the same footer.
hooks/register.tsx 118 lines1import { 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}
118types/index.d.ts 12 lines1export 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