Stops `git add -A` and `commit -a` from sweeping in files Claude never touched, and hands Claude the exact paths to stage instead.

Claude commits what Claude touched. Nothing else.
You had a half-finished experiment in your working tree, a debug script, a local config tweak. Claude ran git add -A, and now all of it is in the commit. Maybe it's in the pushed PR too.
Telling Claude "only stage your own files" works until it doesn't. Broad staging is one keystroke away, and Claude has no record of which files it actually changed.
git add -A, git add ., git add -u, git commit -a and friends are refused before they run.touched-only: broad staging can sweep in unrelated changes. Stage only the files Claude edited this session:
git add -- src/app.ts 'docs/read me.md'
If the user really wants everything staged, ask them to run it.
Prerequisites: Claude Code 2.1.287 or later (claude --version).
/plugin marketplace add KrisnaSantosa15/touched-only
/plugin install touched-only@touched-only
No config needed.
Verify: after Claude edits a file, type /touched. You'll see the files Claude has edited so far.
| Command | Why |
|---|---|
git add -A, git add --all | Stages the whole tree |
git add ., git add ./, git add :/, git add * | Stages everything under a folder |
git add -u, git add --update | Stages every tracked change |
git commit -a, git commit -am "...", git commit --all | Commits every tracked change |
It catches them inside chains too, like cd app && git add -A. Explicit paths, git add -p and plain git commit -m go through.
Files Claude changes from the shell (sed -i, mv, a code generator) aren't tracked, so Claude has to name those paths itself.
Need everything staged? Run git add -A yourself in your own terminal. The guard only checks the commands Claude runs.
Issues and pull requests are welcome. Add a test for any behavior you change.
git clone https://github.com/KrisnaSantosa15/touched-only.git
cd touched-only && claude plugin validate . && claude plugin test .
Try your changes live with claude --plugin-dir ..
MIT. Use it, fork it, ship it.
hooks/register.ts 54 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4const NAME = 'touched-only'
5const files = atom({ plugin: 'touched-only', key: 'files' } as const, [])
6
7const BROAD_STAGING =
8 /\bgit\s+(add\s+([^;&|]*\s)?(-A|--all|-u|--update|\.|\.\/|:\/|\*)(?=\s|$|[;&|])|commit\s+([^;&|]*\s)?(-[b-zB-Z]*a[a-zA-Z]*|--all)(?=\s|$|[;&|]))/
9
10const relative = (cwd: string, path: string) =>
11 path.startsWith(`${cwd}/`) ? path.slice(cwd.length + 1) : path
12
13const quote = (path: string) => (/^[\w./@+-]+$/.test(path) ? path : `'${path.replaceAll("'", "'\\''")}'`)
14
15const touched = async ($: EngineInterface) => {
16 const cwd = await $.session.cwd()
17 return (await read($, files)).map(path => relative(cwd, path))
18}
19
20export const register: Register = on => {
21 on('session.start', async ($, e, next) => {
22 await $.command.register({ name: 'touched', description: 'List the files Claude edited in this session' })
23 return next(e)
24 })
25
26 on('command.run', { command: 'touched' }, async $ => {
27 const list = await touched($)
28 return { text: list.length === 0 ? 'Claude has not edited any files yet.' : list.join('\n') }
29 })
30
31 on('tool.call', async ($, e, next) => {
32 const path = e.tool === 'Edit' || e.tool === 'Write' ? e.file_path : e.tool === 'NotebookEdit' ? e.notebook_path : undefined
33 const ran = await next(e)
34
35 if (path !== undefined && ran.deny === undefined && ran.isError !== true) {
36 await update($, files, list => (list.includes(path) ? list : [...list, path]))
37 }
38
39 return ran
40 })
41
42 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
43 if (!BROAD_STAGING.test(e.command)) return next(e)
44
45 const list = await touched($)
46 const hint =
47 list.length === 0
48 ? 'Claude has not edited any files this session, so name the paths to stage explicitly.'
49 : `Stage only the files Claude edited this session:\n git add -- ${list.map(quote).join(' ')}`
50
51 return { deny: `${NAME}: broad staging can sweep in unrelated changes. ${hint}\nIf the user really wants everything staged, ask them to run it.` }
52 }).catch(($, e, next) => (next.called ? next(e) : { deny: `${NAME}: the guard itself failed, so the command was held.` }))
53}
54types/index.d.ts 8 lines1export type TouchedFiles = string[]
2
3declare module 'claude-code' {
4 interface PluginState {
5 'touched-only': { files: TouchedFiles }
6 }
7}
8