SLOPSHOPPER

cwd-status

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

newspinnerguardstatus
v0.1.0MITupdated 2026-10-02choas/claude-mods/cwd-status
A shopper browsing a rack in a slop shop
README

my-mods

Small Claude Code mods that add live info to the footer, next to the mode labels.

ModShowsExample
cwd-statusThe folder Claude is working in, including where a Bash command cds to๐Ÿ“ ~/prj/app
git-statusBranch, uncommitted files and unpushed commits of that folder's repoโއ main โ—3 โ†‘2

Together they read like ๐Ÿ“ ~/prj/app โއ main โ—3 โ†‘2.

Install

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.

Update

/plugin marketplace update my-mods

Uninstall

/plugin uninstall cwd-status@my-mods
/plugin uninstall git-status@my-mods

cwd-status

Shows ๐Ÿ“ <folder> with your home folder shortened to ~.

  • Follows the session's working folder.
  • When a Bash command cds or pushdes somewhere (cd ../other && make), it shows that folder until the next tool runs.

git-status

Shows the git state of the current folder:

PartMeaning
โއ mainCurrent branch (short commit hash on a detached HEAD)
โ—3Uncommitted entries: modified, staged, renamed, conflicted and untracked
โ†‘2Commits 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.
  • Refreshes when the session starts, on every prompt (so commits made in another terminal show up), after Bash/Edit/Write tool calls, and when the folder changes.
  • Follows cd inside Bash commands on its own, so it works without cwd-status.
  • Uses 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.

Development

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.

License

MIT

Source 2 files
hooks/register.ts 77 lines
1import { 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}
77
types/index.d.ts 8 lines
1export type CwdStatusDir = string | null
2
3declare module 'claude-code' {
4  interface PluginState {
5    'cwd-status': { cwd: CwdStatusDir }
6  }
7}
8