Your environment profile and permissions

Day 2: Loops and your environment · Exercise 3 of 4 · 15 min

Finish with two files: a filled environment profile, and a permissions file whose behaviour on six commands you wrote down before you observed it.

Setup

Every command after the setup block runs from inside day2-work, the copy of your lane at the course root; the shipped sandbox/ is never edited. Paste the block from the course root (if your shell is inside day2-work, cd .. first). It makes the copy if it is missing, installs the blank profile, the prediction grid and the example permissions file, and leaves you inside the copy. Nothing here needs Exercises 1 or 2.

Swift lane
mkdir -p work && { [ -d day2-work ] || cp -R sandbox/swift day2-work; } && cd day2-work && mkdir -p .claude && cp ../day-2-loops-and-your-environment/starter/env-profile.md . && cp ../day-2-loops-and-your-environment/starter/prediction-grid.md . && cp ../day-2-loops-and-your-environment/starter/settings.example.json .claude/settings.json && ls env-profile.md prediction-grid.md .claude
Kotlin lane
mkdir -p work && { [ -d day2-work ] || cp -R sandbox/kotlin day2-work; } && cd day2-work && mkdir -p .claude && cp ../day-2-loops-and-your-environment/starter/env-profile.md . && cp ../day-2-loops-and-your-environment/starter/prediction-grid.md . && cp ../day-2-loops-and-your-environment/starter/settings.example.json .claude/settings.json && ls env-profile.md prediction-grid.md .claude

The block lists env-profile.md, prediction-grid.md, and settings.json under .claude. JSON carries no comments, so the rule-by-rule reading of the example lives in ../day-2-loops-and-your-environment/starter/settings.example.md; step 4 says when to open it. The example replaces the settings.json your lane shipped, and its first two keys, claudeMdExcludes and autoMemoryEnabled, are that file's, carried over. They are not permission rules; leave them as they are.

Steps

  1. Fill the six rows of env-profile.md, one short true sentence each. Start from the Default wording column of its table and change what is false for you. Name the kind of connector, not the vendor: no employer, internal system, hostname or repository name. A connector that later needs a rule of its own is scoped as mcp__<server>, mcp__<server>__*, or mcp__<server>__<tool>.

  2. Edit .claude/settings.json: the deny list first, then ask, then allow, which is the order the engine evaluates rules in. Each rule is shaped Tool(pattern). Keep every shipped rule, including any allow rule you cannot yet justify; step 4 and the reference answers come back to those. You may add rules, but write no allow rule of your own you cannot justify in one sentence. The only rules you remove are the other lane's two allow rules (Bash(swift test:*) and Edit(./Sources/**) for Kotlin, Bash(./gradlew test) and Edit(./src/**) for Swift), since the example carries both lanes' and those match nothing in your copy. Leave settings.example.md closed until your predictions exist.

  3. Fill all six rows of prediction-grid.md before you run anything. For each command write allow, ask or deny, and name the rule you think decides it. Predictions written afterwards teach nothing.

  4. With the predictions written, open settings.example.md for the shipped reason behind any rule. Then check that the file parses and that three deny rules survived your edit: probes 4, 5 and 6 run into them.

    Swift lane
    python3 -m json.tool .claude/settings.json
    grep -nE 'Bash\((rm|gh pr merge|curl):\*\)' .claude/settings.json
    Kotlin lane
    python3 -m json.tool .claude/settings.json
    grep -nE 'Bash\((rm|gh pr merge|curl):\*\)' .claude/settings.json

    The first command prints your settings back, reformatted. The second prints the lines carrying the three rules, which in the unedited example are:

    7:      "Bash(rm:*)",
    8:      "Bash(gh pr merge:*)",
    12:      "Bash(curl:*)",

    Then take a baseline in your own shell, which passes through no permission rules: a green suite now means a red one during the probe is the settings file, not the code.

    Swift lane
    swift test

    The XCTest count line, the one starting Executed, decides it. It is not the last line: Swift Testing prints its own summary after it, Test run with 0 tests, which counts none of these tests, so ignore that one and look a few lines up. A fresh copy ended with these lines on 2026-10-06:

    Test Suite 'All tests' passed at ….
    	 Executed 34 tests, with 1 test skipped and 0 failures (0 unexpected) in … (…) seconds
    …  Test run started.
    …  Testing Library Version: …
    …  Target Platform: …
    …  Test run with 0 tests in 0 suites passed after … seconds.

    If your copy carries Exercise 1's change, the count is higher than 34; any count with 0 failures is the green baseline.

    Kotlin lane
    ./gradlew test

    BUILD SUCCESSFUL is the line that decides it. A first run in a fresh copy printed the lines below on 2026-09-23; a repeat run prints different task counts.

    BUILD SUCCESSFUL in …
    … actionable tasks: …
    Consider enabling configuration cache to speed up this build: https://docs.gradle.org/…/userguide/configuration_cache_enabling.html

    A suite an earlier exercise left red is fine; note it. Now start Claude Code inside day2-work with claude and look at the rules the session loaded.

    /permissions

    That view is the rules in effect for this session, which is not always the file you just edited. If one of the three deny rules is not in it, put it back before you send the probe.

  5. Send the probe, in full, and record what happened for each of the six commands.

    Swift lane
    Attempt each of these six commands in order, one at a time, in this working copy. Do not work around a refusal, and do not edit any settings file. After each one, tell me whether it was allowed, asked, or denied, and quote the rule that decided it. If one is refused, record the refusal and go on to the next. 1: git status. 2: swift test. 3: gh pr view 1. 4: gh pr merge 1. 5: rm -rf build. 6: curl https://example.com
    Kotlin lane
    Attempt each of these six commands in order, one at a time, in this working copy. Do not work around a refusal, and do not edit any settings file. After each one, tell me whether it was allowed, asked, or denied, and quote the rule that decided it. If one is refused, record the refusal and go on to the next. 1: git status. 2: ./gradlew test. 3: gh pr view 1. 4: gh pr merge 1. 5: rm -rf build. 6: curl https://example.com

    Rows 1 and 3 test rules, not repositories, so ignore whatever git status and gh print. Row 3 asks for approval: choose Yes, not “Yes, and don't ask again”. gh pr view only reads, and the row is ask whatever it prints. The Observed column takes what happened, not what the agent said should happen.

  6. Compare the two halves of the grid row by row and mark every mismatch with one sentence: what you believed about the rules, and what turned out to be true. Then put both deliverables where the reset cannot reach them.

    cp env-profile.md prediction-grid.md ../work/ && ls ../work
  7. Only now, open the reveal below.

  8. Part B, observation only; skip it if you have no connectors attached. Run these in order, disabling one connector in the /mcp view, and write down the two /context figures and their difference.

    /context
    /mcp
    /context

If you see something else instead

Likely wrong turns

Self-check

Four lines decide it

Run them in your own shell, from inside day2-work.

python3 -m json.tool .claude/settings.json > /dev/null && echo 'settings parse: ok'
grep -c '^[0-9]\. \*\*.*<' env-profile.md
awk -F'|' '/^\| [1-6] \|/ && $4 ~ /allow|ask|deny/ {n++} END {print n+0}' prediction-grid.md
grep -c 'gh pr view' .claude/settings.json

A pass is settings parse: ok, then 0, then 6, then 0. The second counts profile rows still holding their placeholder; the third counts grid rows with a word in the Predicted column. The fourth prints 0 unless you wrote a gh pr view rule yourself in step 2, because an accepted “don't ask again” is never saved to the shared file, wherever its settings.local.json ended up.

Done when: all six profile rows are filled and none names an employer, a system or a repository; the settings file parses, still carries every shipped rule except the other lane's two allow rules, and every allow rule you added has a sentence you would defend in review; all six grid rows carry a prediction written before its observation, each mismatch annotated with the rule that decided it; and copies of both files are in ../work/.

Reference answers

Row 5, and the rule row 3 can leave behind

Row 5, rm -rf build. This is the row built to disagree with a first reading of the file. The example allows Bash(rm -rf build) and denies Bash(rm:*). The allow rule reads as housekeeping and does nothing: evaluation runs deny, then ask, then allow, and the first match ends the decision regardless of how specific a later rule is. Specificity is never a tiebreak. The row is deny, decided by Bash(rm:*), and the narrow allow underneath it never runs.

That is the failure mode worth carrying out of this exercise. A rule that is written, read in review, and approved can still be inert, and nothing in the file says so. The only way to know is to predict a command and then run it. Row 4, gh pr merge 1, is the same mechanism pointing the way people expect: Bash(gh pr merge:*) in the deny list settles it before the Bash(gh:*) ask rule is ever consulted.

Row 3, gh pr view 1. The row is ask, decided by Bash(gh:*). The rule to watch is the one an accepted “Yes, and don't ask again” saves. It is an allow rule, and in day2-work it changes nothing: the ask rule Bash(gh:*) is evaluated first, the same mechanism as row 5. In a copy without that ask rule, it takes effect. It is not saved into the .claude/settings.json you edited: Claude Code saves it to .claude/settings.local.json at the root of the git repository, and it applies to future sessions anywhere in that repository. Four cases keep the file beside day2-work/.claude/settings.json instead, where the reset removes it with the copy: outside a git repository, as in a zip download; on Windows; when the repository root is your home directory; and when the repository root or its .git or .claude entry is not owned by your user. Otherwise the file is at the repository root, which in a clone of this course is the clone's root folder, not day2-work, so the rule outlives the reset and follows you into Day 3's copy. File-edit approvals are never saved; they last until the session ends.

Wherever it lands, .claude/settings.local.json is personal and outranks the shared .claude/settings.json, so a rule that lives only there is one your teammates do not have and will not see in your diff. A rule the team shares is written into the shared file on purpose, where a reviewer gets to ask what it lets through.

One more thing before you grep your own file: Bash(gh pr view *) and Bash(gh pr view:*) are the same rule in two spellings, so searching for one form can miss a rule that is already there.

Part B: what to expect from the connector probes

This course quotes no /mcp or /context output, on purpose. Your connectors do not exist on the machine this course was built on, and the documentation read on 2026-09-22 does not state the format of the context breakdown, so any example printed here would be invented. What you record is your own two figures.

Each attached server's tool names are in the window from the start of the session, with the full schemas deferred by default. This course has not recorded what disabling a server partway through a session does to the figures, so the pair you wrote down is the finding, whatever it shows. If the two figures match, check that /mcp showed the server as attached before you disabled it.

If /mcp shows nothing attached, there is nothing here to measure. Write the honest answer in row 1 of your profile, which is the row that matters most, and move on to Exercise 4.

Reset

Every Day 2 exercise resets the same way: quit Claude Code, then from inside day2-work run your lane's line.

Swift lane
cd .. && rm -rf day2-work && cp -R sandbox/swift day2-work && cd day2-work
Kotlin lane
cd .. && rm -rf day2-work && cp -R sandbox/kotlin day2-work && cd day2-work

It replaces the copy with a fresh one from the shipped lane and leaves ../work alone, so step 6's copies survive. A fresh copy is the shipped lane and nothing else, so re-run this page's setup block before you continue. It does not reach a .claude/settings.local.json at a clone's root folder; in the four cases the reveal lists, the file is inside day2-work and goes with the copy. You need not reset before Exercise 4, which edits the settings file you wrote here.

Sources