NRT Agentic AI Michigan State University

Demo 3 · Level 3 — Permission-controlled access

MCP + Permissions

The MCP server stays the approved route; the direct ones close. You will write a reasonable policy, break it yourself, and find out why the blunt version is the right one.

Download Demo 3  ↓ ← Demo 2 Demo 4  →

← Back to all four demos

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:

  1. In your own workflow, what must the agent never touch directly?
  2. 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 →