Shows the folder Claude is working in on the status line.

Small Claude Code mods that add live info to the footer, next to the mode labels.
| Mod | Shows | Example |
|---|---|---|
| cwd-status | The folder Claude is working in, including where a Bash command cds to | ๐ ~/prj/app |
| git-status | Branch, uncommitted files and unpushed commits of that folder's repo | โ main โ3 โ2 |
Together they read like ๐ ~/prj/app โ main โ3 โ2.
In Claude Code, add this marketplace once:
/plugin marketplace add choas/claude-mods
Then install one or both mods:
/plugin install cwd-status@my-mods
/plugin install git-status@my-mods
Run /reload-plugins (or restart Claude Code) and the labels show up in the footer.
You can also browse and install them interactively with /plugin โ Marketplaces โ my-mods.
/plugin marketplace update my-mods
/plugin uninstall cwd-status@my-mods
/plugin uninstall git-status@my-mods
Shows ๐ <folder> with your home folder shortened to ~.
cds or pushdes somewhere (cd ../other && make), it shows that folder until the next tool runs.Shows the git state of the current folder:
| Part | Meaning |
|---|---|
โ main | Current branch (short commit hash on a detached HEAD) |
โ3 | Uncommitted entries: modified, staged, renamed, conflicted and untracked |
โ2 | Commits not pushed yet: ahead of the upstream, or, without an upstream, commits on no remote |
โ and โ only appear when non-zero; outside a git repo nothing is shown.cd inside Bash commands on its own, so it works without cwd-status.git status --porcelain=v2 --branch with optional locks off and a 5 s timeout, so it never blocks your repo.Requires git on your PATH.
Each mod is a plugin folder with a hooks module (hooks/register.ts), a state contract (types/index.d.ts) and tests:
claude plugin validate git-status
claude plugin test git-status
To try local changes, point Claude Code at the folder: claude --plugin-dir ./git-status.
MIT
hooks/register.ts 77 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const cwd = atom({ plugin: 'cwd-status', key: 'cwd' } as const, null)
5
6// Joins `path` onto `base` the way `cd` would: `~` is home, `.` and `..` fold away.
7export function resolveDir(base: string, path: string, home: string | undefined): string {
8 const expanded = home && (path === '~' || path.startsWith('~/')) ? home + path.slice(1) : path
9 const parts: string[] = []
10 for (const part of (expanded.startsWith('/') ? expanded : `${base}/${expanded}`).split('/')) {
11 if (part === '..') parts.pop()
12 else if (part !== '' && part !== '.') parts.push(part)
13 }
14 return `/${parts.join('/')}`
15}
16
17// The folder the last top-level `cd`/`pushd` in a Bash command leaves it in, if any.
18export function cdTarget(command: string, base: string, home: string | undefined): string | undefined {
19 let dir: string | undefined
20 for (const step of command.split(/&&|\|\||;|\n/)) {
21 const m = step.trim().match(/^(?:cd|pushd)(?:\s+(?:--\s+)?("[^"]*"|'[^']*'|\S+))?\s*$/)
22 if (!m) continue
23 const arg = m[1]?.replace(/^(["'])(.*)\1$/, '$2')
24 if (arg === '-') continue
25 dir = arg === undefined ? home : resolveDir(dir ?? base, arg, home)
26 }
27 return dir
28}
29
30export function tilde(dir: string, home: string | undefined): string {
31 return home && (dir === home || dir.startsWith(`${home}/`)) ? `~${dir.slice(home.length)}` : dir
32}
33
34async function show($: EngineInterface, dir: string) {
35 const label = tilde(dir, await $.env.get('HOME'))
36 if ((await read($, cwd)) !== label) await update($, cwd, () => label)
37}
38
39export const register: Register = on => {
40 on('session.start', async ($, e, next) => {
41 const result = await next(e)
42 $.ui.status(undefined) // clear the line an earlier version pinned
43 await show($, await $.session.cwd())
44 return result
45 })
46
47 // A `cd` inside a Bash command moves that command only (the shell is reset when it
48 // leaves the project), so show where it runs, and keep that until the next tool.
49 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
50 const before = await $.session.cwd()
51 const target = cdTarget(e.command, before, await $.env.get('HOME'))
52 if (target) await show($, target)
53 const result = await next(e)
54 const after = await $.session.cwd()
55 await show($, after !== before || !target ? after : target)
56 return result
57 })
58
59 on('tool.call', async ($, e, next) => {
60 const result = await next(e)
61 if (e.tool !== 'Bash') await show($, await $.session.cwd())
62 return result
63 })
64
65 on('classic.CwdChanged', async ($, e, next) => {
66 const result = await next(e)
67 await show($, e.new_cwd)
68 return result
69 })
70
71 // Shown with the footer's mode labels, without a plugin-name prefix.
72 on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
73 const dir = await read($, cwd)
74 return dir === null ? next(e) : next({ ...e, props: { ...e.props, modes: [...e.props.modes, `๐ ${dir}`] } })
75 })
76}
77types/index.d.ts 8 lines1export type CwdStatusDir = string | null
2
3declare module 'claude-code' {
4 interface PluginState {
5 'cwd-status': { cwd: CwdStatusDir }
6 }
7}
8