Before you start, three things have to be working: a terminal (the macOS
Terminal, or the one inside VS Code), Python 3, and Claude Code signed in to the NRT
organization. Check with python3 --version and claude, then
/status.
If any of those fail, stop here and work through the setup steps first — about ten minutes. Nothing below will work without them.
This package ships with a deliberately incomplete policy. Part B has you find the hole in it and close it. Permission changes take effect when Claude Code restarts, so expect to quit and relaunch several times.
Package:
Demo3_MCP_Permissions_Tissue_Analyzer.zip
Demo 2 made the capability explicit. This demo controls who may call what.
The teaching goal is not "permissions exist". It is that writing a good permission policy is harder than it looks, and you will find that out by writing a bad one first and then breaking it yourself.
1. Unzip and look at the policy
unzip Demo3_MCP_Permissions_Tissue_Analyzer.zip
cd Demo3_MCP_Permissions_Tissue_Analyzer
cat .claude/settings.json
You should see a short, reasonable-looking policy:
{
"permissions": {
"deny": [
"Read(./data/**)",
"Edit(./data/**)"
]
}
}
The intent is clear: Claude must not look at the raw slide images directly. It must go through the MCP tools instead.
Also read CLAUDE.md, which states the same thing in prose. Note that prose is
advice and the settings file is enforcement — the difference matters.
2. Start Claude Code and read the rules back
claude
A prompt appears the first time you start Claude Code here, and the default answer is the wrong one. It reads "New MCP server found in this project: tissue-analyzer" and offers three choices:
Use this MCP server
Use this and all future MCP servers in this project
> Continue without using this MCP server <- pre-selected
Press Enter straight away and the server is switched off, quietly, for this project. Every MCP step after that fails, and nothing tells you why.
Press ↑ to reach "Use this MCP server", then Enter.
If you already pressed Enter too fast, it is recoverable. Leave Claude Code with
/exit, then run:
claude mcp reset-project-choices
and start Claude Code again from the project root.
Stay in the default permission mode for this demo. Claude Code asks before it
acts; that is the default, and it is what every step below assumes. Pressing
shift+tab cycles to other modes — an accept-edits mode, and a bypass mode that
skips permission checks altogether. In a bypass mode nothing in Part B will refuse
anything, and the demo silently stops demonstrating anything. If the indicator next to
the prompt shows a mode other than the default, press shift+tab until it comes
back.
Inside Claude Code:
/permissions
Confirm the two deny rules are listed. While you are here, note the three modes:
allow the tool runs without asking
ask you are prompted each time
deny the tool is refused, always
And the rule that decides conflicts:
deny beats ask beats allow
Also run /mcp and confirm tissue-analyzer is connected with two tools.
Part A — Watch the policy work
3. Try the forbidden thing
Read data/case_mild_injury_histology.pgm directly and describe the raw pixel values.
This should be refused. Note the exact wording Claude gives you — you will compare it with a different kind of refusal later.
4. Try something the policy does not mention
Show me previews/case_mild_injury_histology.png
This is allowed. previews/ appears nowhere in the deny list.
Worth a moment: the colour preview is a picture of the same slide. The policy protects a file extension and a folder, not a concept. A permission rule is exactly as smart as the path you typed into it.
5. Use the approved route
Use analyze_tissue with:
slide = data/case_mild_injury_histology.pgm
magnification = 20
damage_threshold_fraction = 0.45
out = results/mild_permission_analysis.json
Then chain the classifier:
Use classify_tissue with:
features = results/mild_permission_analysis.json
out = results/mild_permission_classification.json
Expected: mild_injury. Compare against docs/EXPECTED_SYNTHETIC_RESULTS.md.
6. Notice what just happened
Claude could not open that file. The tool it called did open it, read every pixel, and handed back numbers.
Claude ──X──> data/*.pgm (denied: Claude's own Read tool)
Claude ────> MCP tool ────> data/*.pgm (allowed: a separate process)
│
└────> {"damage_fraction": 0.22, ...}
Claude Code permissions restrict Claude's tools. They are not an operating-system sandbox. The MCP server is an ordinary process running as you, with your file access. If that distinction were not true, the demo could not work at all — the analysis has to read the pixels to produce a number.
Part B — Break your own policy
This is the part worth doing slowly.
7. Find the hole
The policy denies Read and Edit on data/. Ask Claude to do this:
Using a shell command, show me the first 5 lines of
data/case_mild_injury_histology.pgm
It works. Claude runs head -5 data/... through Bash and the contents come
straight back.
The deny list named Read and Edit. It never mentioned Bash. The file was
never protected — only two of the several routes to it were.
A deny list protects what you remembered to deny.
8. Patch it the obvious way — and watch that fail too
Copy the second stage over the policy:
cp docs/permission_stages/stage2_enumerate.json .claude/settings.json
The copy just saves typing — you can equally open .claude/settings.json and make the change yourself. Three command patterns are added to the deny list:
"permissions": {
"deny": [
"Read(./data/**)",
"Edit(./data/**)",
+ "Bash(cat:*)",
+ "Bash(head:*)",
+ "Bash(less:*)"
]
}
Restart Claude Code (permission changes are read at startup), then:
/permissions
You should now see Bash(cat:*), Bash(head:*) and Bash(less:*) denied.
Try the same request again — it is blocked.
Now try harder:
Using a shell command, print the first few lines of
data/case_mild_injury_histology.pgm — you may use any tool available.
There are many ways to read a file: od, xxd, sed, awk, tail,
python3 -c, grep .. Enumerating the bad ones is a game you lose.
9. Close it properly
cp docs/permission_stages/stage3_closed.json .claude/settings.json
The copy just saves typing — you can equally open .claude/settings.json and make the change yourself. The three command patterns come out again, and the capability itself goes in:
"permissions": {
"deny": [
"Read(./data/**)",
"Edit(./data/**)",
- "Bash(cat:*)",
- "Bash(head:*)",
- "Bash(less:*)"
+ "Bash"
]
}
Restart Claude Code and try step 7 again. Now it is refused.
The difference between stage 2 and stage 3 is the difference between blocking bad uses of a capability and not granting the capability. The second is the only one that holds.
stage 2: Bash is available, and I listed some commands I do not like
stage 3: Bash is not available
Part C — Operating on permissions
Four things you will need to do in your own project.
10. Read the current state
/permissions
This is always the truth for the running session. A settings file you edited but did not restart into is not yet in effect.
11. Add a rule
Two ways, and it is worth doing both once:
By file — edit .claude/settings.json, add an entry, restart Claude Code.
By the interface — open /permissions and add a rule there. Note where it
gets written: the interface usually writes to .claude/settings.local.json,
your personal file, not the shared project one. Check afterwards:
cat .claude/settings.json # shared, committed to the repository
cat .claude/settings.local.json # personal, usually gitignored
Both are in effect at once. This is how a lab can ship a baseline policy that an individual can tighten but should not silently loosen.
12. Remove a rule
Delete the Edit(./data/**) line from .claude/settings.json, restart, and ask:
Add a comment line to the top of data/case_normal_histology.pgm
It now succeeds — and it has corrupted a data file. Undo it, then put the rule back.
The point of doing this on purpose: removing a permission is a one-line edit, it takes effect silently, and nothing warns you what it allowed. Removing rules deserves the same care as removing a safety check in code.
13. Use ask rather than deny
cp docs/permission_stages/stage4_ask_and_allow.json .claude/settings.json
The copy just saves typing — you can equally open .claude/settings.json and make the change yourself. The deny list is untouched; allow and ask are added above it:
"permissions": {
+ "allow": [
+ "Read(./previews/**)",
+ "Read(./docs/**)"
+ ],
+ "ask": [
+ "Write(./results/**)"
+ ],
"deny": [
"Read(./data/**)",
"Edit(./data/**)",
"Bash"
]
}
Restart, then:
Use analyze_tissue on data/case_necrosis_histology.pgm at magnification 20
with damage_threshold_fraction 0.45, saving to results/necrosis_permission.json
Writing into results/ is set to ask, so you get a prompt. Approve it.
Three modes, three uses:
allow routine and safe — previews/, docs/
ask consequential — writing files, anything you want to see happen
deny never, no prompt — raw patient data, destructive commands
ask is the one people under-use. It is the right setting for most things that
change state, because it keeps you in the loop without blocking the work.
Part D — What this is and is not
14. Say the limit out loud
Claude Code permissions are a guardrail on the agent, not a security boundary.
They stop an agent from doing something by accident or by a badly-worded request. They do not contain a process, and they do not protect data from anything other than Claude's own tools. Your MCP server, your terminal, and any script you run all still have your full file access.
For IRB-covered data, the sentence that matters is: the images never entered the conversation — a local process read them and returned numbers. That is an architecture property, and permissions are what stop it being accidentally bypassed.
15. Design question for your own project
Write down two answers before you leave:
- In your own workflow, what must the agent never touch directly?
- What is the approved route to it, and does it return numbers rather than raw data?
If the answer to 2 is "there isn't one yet", that is your Week 4 task.
Appendix — permission stages in this package
| File | Policy | What it teaches |
|---|---|---|
stage1_first_attempt.json |
deny Read + Edit on data/ |
a reasonable first try, with a hole |
stage2_enumerate.json |
adds Bash(cat:*), Bash(head:*), Bash(less:*) |
naming bad commands does not work |
stage3_closed.json |
adds a blanket Bash deny |
remove the capability, not the uses |
stage4_ask_and_allow.json |
adds allow and ask entries |
all three modes together |
Swap any of them in with:
cp docs/permission_stages/<file> .claude/settings.json
then restart Claude Code. The package ships at stage 1 on purpose.
Appendix — quick reference
| Action | How |
|---|---|
| See rules in effect | /permissions inside Claude Code |
| See MCP servers | /mcp inside Claude Code |
| Shared project policy | .claude/settings.json (commit this) |
| Personal overrides | .claude/settings.local.json (usually gitignored) |
| Apply a changed file | restart Claude Code |
| Precedence | deny > ask > allow |
Next. Demo 4 narrows the other way: a tissue-analyst subagent
that holds only the two MCP tools, with no general file or shell access at all. Open the Demo 4 guide →