Stops Claude from opening your skill, rule and plugin folders file by file while you have the fence switched on. Skills still run normally.

A Claude Code mod that stops Claude from opening your skill, rule and plugin folders file by file, while you have the fence switched on.
Claude already sees the name and a short description of every skill you have installed. Opening a skill's folder to read its files adds a lot more text to the conversation, and that text is sent again with every later message. The fence keeps Claude from doing that by accident.
Example: you ask Claude to "look at what skills I have". Without the fence it may open dozens of skill files. With the fence on, it is told to use the list it already has, and to run a skill with the Skill tool when it needs one.
Paste these two lines into a terminal (not into Claude):
claude plugin marketplace add varunmoka7/read-fence
claude plugin install read-fence@read-fence
Needs Claude Code 2.1.287 or newer (mods are on by default from that version). Works on macOS and Linux. Windows paths are not handled, so the fence will not block anything there.
One command, inside Claude Code:
/read-fence on switches the fence on./read-fence off switches it off./read-fence status (or just /read-fence) tells you whether it is on.The fence starts off.
While it is on, Claude's Read, Glob, Grep and Bash tools are refused when they point at:
skills, rules and plugins/cache folders inside your Claude config folder (~/.claude, or the folder named by CLAUDE_CONFIG_DIR).claude/skills and .claude/rules folders of the project you are working inThese are always allowed, even with the fence on:
CLAUDE.mdread-fence.json in your Claude config folder./read-fence on or /read-fence off.Switch the fence off with /read-fence off, or remove the mod:
claude plugin uninstall read-fence@read-fence
You can delete read-fence.json from your Claude config folder afterwards.
$HOME) or uses a path relative to the current folder can get past it.MIT
hooks/read-fence.mjs 121 lines1// Read Fence: while on, Read, Glob, Grep and Bash may not touch the folders that
2// already enter the context as a listing (skills, rules, plugin cache). Off by
3// default. The switch lives in read-fence.json inside the Claude config folder
4// (CLAUDE_CONFIG_DIR, or ~/.claude when that is not set).
5//
6// Exempt even when on: the skill the model just invoked (its folder, learnt from
7// skill.prompt), any CLAUDE.md, and the memory folders under the config folder.
8
9const SETTINGS = "read-fence.json";
10const BLOCKED_IN_CONFIG = ["plugins/cache", "skills", "rules"];
11const BLOCKED_IN_PROJECT = [".claude/skills", ".claude/rules"];
12
13// Folders of skills invoked this session (absolute, from the skill prompt's base dir line)
14const invoked = new Set();
15
16export function register(on) {
17 on("session.start", async ($, e, next) => {
18 await $.command.register({
19 name: "read-fence",
20 description: "Turn the read fence on or off, or show its state: on, off or status",
21 argumentHint: "on|off|status",
22 });
23 return next(e);
24 });
25
26 on("command.run", ($, e, next) => runCommand($, e, next).catch(() => next(e)));
27
28 on("skill.prompt", async ($, e, next) => {
29 const m = /Base directory for this skill:\s*(\S+)/.exec(String(e.text ?? ""));
30 if (m) invoked.add(m[1].replace(/\/+$/, ""));
31 return next(e);
32 });
33
34 // The fence is a convenience: if it cannot decide, the call goes through.
35 on("tool.call", { tool: ["Read", "Glob", "Grep", "Bash"] }, ($, e, next) => guard($, e, next).catch(() => next(e)));
36}
37
38async function runCommand($, e, next) {
39 if (e.command !== "read-fence") return next(e);
40 const arg = String(e.args ?? "").trim().toLowerCase();
41 if (arg === "on" || arg === "off") {
42 try {
43 await setOn($, arg === "on");
44 } catch {
45 return { text: `read-fence could not save its setting, so it is still ${(await isOn($)) ? "on" : "off"}` };
46 }
47 return { text: `read-fence ${arg}` };
48 }
49 return { text: `read-fence is ${(await isOn($)) ? "on" : "off"}` };
50}
51
52async function guard($, e, next) {
53 if (!(await isOn($))) return next(e);
54 const home = await $.env.get("HOME");
55 const config = await configDir($);
56 const root = await $.session.root();
57 const cwd = await $.session.cwd();
58 for (const p of await touched($, e, cwd, home)) {
59 const why = await blocked($, p, config, root);
60 if (why) {
61 return {
62 deny: `Read Fence refused ${e.tool} on ${p}: ${why}. The fence is on (/read-fence off lifts it). Skill folders enter the context as name and description already; invoke a skill with the Skill tool instead of reading its folder.`,
63 };
64 }
65 }
66 return next(e);
67}
68
69async function configDir($) {
70 const set = await $.env.get("CLAUDE_CONFIG_DIR");
71 const dir = set || `${await $.env.get("HOME")}/.claude`;
72 return dir.replace(/\/+$/, "");
73}
74
75async function isOn($) {
76 try {
77 return JSON.parse(await $.fs.read(`${await configDir($)}/${SETTINGS}`)).on === true;
78 } catch {
79 return false;
80 }
81}
82
83async function setOn($, on) {
84 await $.fs.write(`${await configDir($)}/${SETTINGS}`, JSON.stringify({ on }) + "\n");
85}
86
87/** The absolute paths a call reaches, as spelled. */
88async function touched($, e, cwd, home) {
89 const abs = (p) => (p.startsWith("/") ? p : `${cwd}/${p}`);
90 if (e.tool === "Read") return [abs(String(e.file_path ?? ""))];
91 if (e.tool === "Glob" || e.tool === "Grep") {
92 const base = abs(String(e.path ?? "."));
93 const out = [base];
94 const pat = String(e.pattern ?? "");
95 // A Glob pattern can reach a folder by itself, absolute or relative to the search path.
96 if (e.tool === "Glob" && pat) out.push(pat.startsWith("/") ? pat.split("*")[0] : `${base}/${pat.split("*")[0]}`);
97 return out;
98 }
99 // Bash: every absolute or ~ path spelled in the command line
100 const command = String(e.command ?? "").replace(/\$\{HOME\}|\$HOME/g, home);
101 const found = command.match(/(?:~|\/)[\w./@-]+/g) ?? [];
102 return found.map((p) => (p.startsWith("~") ? home + p.slice(1) : p));
103}
104
105/** Why `path` is fenced, or null. Spelled path decides exemptions; spelled and real path decide blocks. */
106async function blocked($, path, config, root) {
107 const under = (p, dir) => p === dir || p.startsWith(dir + "/");
108 if (/(^|\/)CLAUDE\.md$/.test(path)) return null;
109 if (path === `${config}/${SETTINGS}`) return null;
110 if (/\/projects\/[^/]+\/memory(\/|$)/.test(path) && under(path, config)) return null;
111 for (const dir of invoked) if (under(path, dir)) return null;
112
113 const real = (await $.fs.stat(path, { resolve: true }).catch(() => undefined))?.realPath;
114 for (const p of real && real !== path ? [path, real] : [path]) {
115 for (const b of BLOCKED_IN_CONFIG) if (under(p, `${config}/${b}`)) return `${b} is fenced`;
116 for (const b of BLOCKED_IN_PROJECT) if (under(p, `${root}/${b}`)) return `${b} is fenced`;
117 for (const dir of invoked) if (under(p, dir)) return null;
118 }
119 return null;
120}
121