Blocks reads of .env files and printing of API keys, with a red warning above the prompt.

These are the mods from the video "Claude Code Mods: 3 Official Mods + 3 I Built Without Writing Code". Claude Code wrote each one from a single sentence. They were tested in Claude Code 2.1.288.
| Mod | What it does | Prompt used |
|---|---|---|
secret-guard | Blocks reads of .env files and printing API keys, and shows a red warning above the prompt | Make me a mod called secret-guard. If Claude tries to read a .env file or print an API key, block it and show a red warning above the prompt saying what it blocked. |
checkpoint | Commits the files Claude edited after every turn, and adds /rollback to undo the last checkpoint | Make me a mod called checkpoint. After every turn where Claude edited files, commit the changes to git as a checkpoint and show a toast. Add a /rollback command that undoes the last checkpoint. |
touched | /touched opens a pane listing every file Claude read or edited this session | Make me a mod with a /touched command. It opens a pane that lists every file Claude has read or edited in this session, with a count for each. (then: show paths relative to the project folder and put edited files in orange) |
A mod runs with your permissions and isn't sandboxed. Read what each one does first:
claude plugin validate ./secret-guard
The calls: line lists everything the mod can do. None of these three make network requests or read environment variables.
Requires Claude Code 2.1.287 or later (claude update). Inside a Claude Code session:
/plugin install secret-guard --marketplace sverma24/claude-code-mods
Or from your shell:
claude plugin marketplace add sverma24/claude-code-mods
claude plugin install secret-guard@sauravgenai
Swap secret-guard for checkpoint or touched. Run /reload-plugins in an open session, or start a new one.
Clone this repo, then:
claude --plugin-dir ./secret-guard
That loads it for one session only. Run /plugin and you'll see 1 mod active.
secret-guard matches patterns in the tool call. A command that builds the path or variable name indirectly can get past it. Treat it as a seatbelt, not a vault.checkpoint only tracks files changed with the Edit and Write tools, not files changed through shell commands. /rollback refuses if you have uncommitted changes.hooks/register.tsx 83 lines1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Block } from '../types'
5
6const last = atom({ plugin: 'secret-guard', key: 'last' } as const, null)
7
8const ENV_FILE = /(^|[\/\s'"=])\.env(\.[\w.-]+)?($|[\s'"])/
9const ENV_SAMPLE = /\.env\.(example|sample|template)\b/
10const KEY_VAR = /\$\{?[A-Za-z0-9_]*(API_?KEY|SECRET|TOKEN|PASSWORD)[A-Za-z0-9_]*\}?/i
11const KEY_LITERAL =
12 /\b(sk-[A-Za-z0-9_-]{16,}|sk-ant-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/
13const DUMP_ENV = /(^|[;&|]\s*)(printenv|env|export\s+-p|set)\s*($|[;&|])/
14const PRINTS = /\b(echo|printf|cat|print|printenv)\b/
15
16const isEnvPath = (p: unknown): p is string =>
17 typeof p === 'string' && ENV_FILE.test(p) && !ENV_SAMPLE.test(p)
18
19const inspect = (e: Record<string, any>): { reason: string; target: string } | null => {
20 for (const key of ['file_path', 'path', 'pattern', 'notebook_path']) {
21 if (isEnvPath(e[key])) return { reason: 'Read of a .env file', target: e[key] }
22 }
23
24 const cmd = e.command
25 if (typeof cmd === 'string') {
26 if (ENV_FILE.test(cmd) && !ENV_SAMPLE.test(cmd)) {
27 return { reason: 'Command touching a .env file', target: cmd.slice(0, 60) }
28 }
29 if (KEY_LITERAL.test(cmd) && PRINTS.test(cmd)) {
30 return { reason: 'Printing an API key', target: cmd.slice(0, 60) }
31 }
32 if (PRINTS.test(cmd) && KEY_VAR.test(cmd)) {
33 return { reason: 'Printing an API key variable', target: cmd.slice(0, 60) }
34 }
35 if (DUMP_ENV.test(cmd)) {
36 return { reason: 'Dumping environment variables', target: cmd.slice(0, 60) }
37 }
38 }
39
40 return null
41}
42
43export const register: Register = on => {
44 on('tool.call', async ($, e, next) => {
45 const hit = inspect(e as Record<string, any>)
46
47 if (hit === null) {
48 return next(e)
49 }
50
51 const block: Block = { tool: String(e.tool), ...hit }
52 await update($, last, () => block)
53
54 return {
55 deny: `secret-guard blocked this: ${hit.reason}. Do not read .env files or print secrets.`,
56 }
57 })
58
59 on('prompt.submit', async ($, e, next) => {
60 await update($, last, () => null)
61
62 return next(e)
63 })
64
65 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
66 const block = await read($, last)
67
68 if (block === null) {
69 return next(e)
70 }
71
72 const { Box, Text } = $.ui.resolve(e)
73
74 return (
75 <Box>
76 <Text color="red" bold>
77 🛡 secret-guard blocked {block.tool}: {block.reason} ({block.target})
78 </Text>
79 </Box>
80 )
81 })
82}
83types/index.d.ts 8 lines1export type Block = { tool: string; reason: string; target: string }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'secret-guard': { last: Block | null }
6 }
7}
8