CLAUDE LABJP
2.1.269 — Prompt suggestions were being dropped for Japanese, Chinese, Thai and other languages written without spaces between words. That is now fixedKB5124008 — After September's cumulative update, Windows 11 users report Cowork failing to mount any host folder at all. The VM still starts, which makes the cause hard to pin downPUSH — Some cloud and Cowork sessions have git push rejected by the proxy before it reaches GitHub. Cloning still works, so it reads like a token permission problem when it is notNEW — Adding more material made Projects answer thinner. Three questions we now use to decide which knowledge files stayTOKENS — You cannot price a PDF before sending it: CountTokens does not accept document input. That leaves estimating from page count or extracting the text and counting thatCLEANUP — Before asking an agent to tidy up, separate the work that only needs reading from the work that needs writing. The order you hand over folders cannot be reconsidered afterwards2.1.269 — Prompt suggestions were being dropped for Japanese, Chinese, Thai and other languages written without spaces between words. That is now fixedKB5124008 — After September's cumulative update, Windows 11 users report Cowork failing to mount any host folder at all. The VM still starts, which makes the cause hard to pin downPUSH — Some cloud and Cowork sessions have git push rejected by the proxy before it reaches GitHub. Cloning still works, so it reads like a token permission problem when it is notNEW — Adding more material made Projects answer thinner. Three questions we now use to decide which knowledge files stayTOKENS — You cannot price a PDF before sending it: CountTokens does not accept document input. That leaves estimating from page count or extracting the text and counting thatCLEANUP — Before asking an agent to tidy up, separate the work that only needs reading from the work that needs writing. The order you hand over folders cannot be reconsidered afterwards
Articles/Claude Code
Claude Code/2026-07-30Advanced

You Added a Second Working Root — Now Ask How Far Your Ignore Rules and Deny Patterns Actually Reach

Adding a working root mid-session does not carry your ignore rules or deny patterns with it. Measured evidence from git check-ignore and a three-way matcher comparison, plus the scope-delta audit script I now run from the DirectoryAdded hook.

Claude Code253hooks18MonorepoSecurity12Operations18

Premium Article

I ran /add-dir mid-session to pull in a second repository, because I wanted the iOS build configuration side by side with what I was already editing.

Working as an indie developer with an app repository on one side and a site repository on the other, this kind of crossing comes up constantly.

The editing part went fine. What bothered me came after.

I ran my usual sweep for files that might carry credentials, and the count had not moved. The root I had just added contains a pile of configuration files. The number should have changed.

A few minutes of digging gave a boring answer. The sweep was still anchored to the first root. Adding a root widens what you can edit; it does not carry the assumptions hanging off the old root along with it.

Session healthy. No errors. Just a safety net stretched across the wrong ground.

An Added Root Is a Sibling, Not a Child

When you add a root with /add-dir or the SDK's register_repo_root, it feels like the working area expanded.

What actually exists is a second independent root sitting alongside the first. Siblings, not parent and child.

The distinction bites hardest on ignore rules. Git exclusion rules stop at the repository boundary. That is unremarkable as a specification, and it becomes an operational hole the moment you have two roots.

I rebuilt the situation locally to check. root_a gets a .gitignore that excludes secrets/; root_b gets none. Both contain the same secrets/prod.env.

# root_a: the rule applies
$ git -C root_a check-ignore -v secrets/prod.env
.gitignore:1:secrets/	secrets/prod.env
 
# asking root_a about a file in root_b is simply refused
$ cd root_a && git check-ignore -v ../root_b/secrets/prod.env
fatal: ../root_b/secrets/prod.env: '../root_b/secrets/prod.env' is outside repository
 
# in root_b, the same path is not excluded at all
$ git -C root_b check-ignore -v secrets/prod.env
(no output, exit code 1)

git status makes the gap visible.

$ git -C root_a status --porcelain
A  .gitignore
A  src/app.ts
 
$ git -C root_b status --porcelain
A  secrets/prod.env staged in the added root
A  src/app.ts

In root_a the file never appears. In root_b it walks straight through to staged.

The practical rule: any judgement about an added root has to run git inside that root. Code that peeks in from the parent with a relative path returns a quietly wrong answer, and the answer it returns is "safe".

The Same Deny Pattern Means Different Things to Different Matchers

Ignore rules were the first surprise. permissions.deny was the second.

Deny is a separate system — string pattern matching rather than git semantics. And here an assumption broke: the same pattern gives different results depending on which matcher reads it.

I lined up three that people actually reach for — fnmatch, Path.match, and glob's ** semantics — and pointed them at the same paths.

import fnmatch, re
from pathlib import PurePosixPath
 
def m_glob(p, pat):
    """Reproduce glob's ** semantics: zero or more directories."""
    rx = (re.escape(pat)
          .replace(r"\*\*/", "(?:[^/]+/)*")
          .replace(r"\*\*", ".*")
          .replace(r"\*", "[^/]*")
          .replace(r"\?", "[^/]"))
    return re.fullmatch(rx, p) is not None
 
for pat in ["*.env", "**/*.env", "config/*.env", "**/config/*.env"]:
    for p in ["config/prod.env", "ios/Config/secrets.env",
              ".vscode/settings.json", "scripts/build.sh"]:
        print(pat, p,
              fnmatch.fnmatch(p, pat),
              PurePosixPath(p).match(pat),
              m_glob(p, pat))

Here is what came back.

PatternPathfnmatchPath.matchglob(**)
*.envconfig/prod.envTrueTrueFalse
*.envios/Config/secrets.envTrueTrueFalse
**/*.envconfig/prod.envTrueTrueTrue
**/*.envios/Config/secrets.envTrueTrueTrue
config/*.envconfig/prod.envTrueTrueTrue
config/*.envios/Config/secrets.envFalseFalseFalse
**/config/*.envconfig/prod.envFalseFalseTrue
**/config/*.envios/Config/secrets.envFalseFalseFalse

Two rows deserve attention.

The *.env rows: under fnmatch and Path.match the pattern matches config/prod.env. The * crosses the slash. If you wrote it with glob in mind, thinking "top level only", it reaches further than intended.

The **/config/*.env rows: glob matches, the other two do not. **/ is read as "at least one directory segment" rather than "zero or more", so a config/ sitting directly under the root falls out.

That second one runs opposite to what I expected. I had been adding **/ prefixes on the assumption that they only ever widen coverage. Depending on the matcher they narrow it — and the thing they drop is the top level of the added root, which is exactly where configuration files cluster.

With a single root you never meet this. There is only one directory tree, shallow paths are rare, and every spelling of the pattern agrees. Add a second, shallower tree and the disagreement surfaces.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Measured proof that ignore rules do not compose across roots — the same secrets/ directory is skipped in one root and staged in the other
A matcher comparison table showing the same deny pattern diverging — `*.env` crosses slashes, and `**/config/*.env` misses files directly under the added root
The ~70-line scope-delta audit run from DirectoryAdded, with its real output: 2 uncovered secret candidates, 1 case mismatch, 2 path collisions
Timed at 21.8 ms on a 2,882-file added root — and why the delta audit is not actually faster than a full rescan (under 1% apart)
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

Related Articles

Claude Code2026-09-05
My deny rule was watching a filename, not the routes that reach it
One line in deny let several other spellings of the same file walk right past. Here is the small matrix I build to list every route to a protected file, and how I re-check it on upgrade day.
Claude Code2026-09-03
Aligning Log Timezones at Display Time, or Fixing Them at Write Time
AdMob, the store reports, and my own logs each ended the day at a different moment. Here is how I went back and forth between display-side and write-side timezone handling, and what I settled on.
Claude Code2026-09-02
Two hooks that record every model switch and stop the ones you never agreed to
Build a PostModelSwitch hook that logs every model change and a PreModelSwitch hook that blocks unapproved switches during unattended runs. Complete scripts, measured timings, and the reason the two jobs must stay separate.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links