CLAUDE LABJP
MEMORY — Claude Code closes a cluster of long-session memory leaks, including MCP stdio stderr piling up to 64 MB per server and LSP documents staying open indefinitelyTABLES — Very large markdown tables no longer stall rendering; tables over 200 rows now show the first 200 with a "… N more rows" noticeSPEED — Sessions carrying many deny/ask permission rules no longer lose seconds every turn: rule matchers are compiled once and cachedTOOLS — print/SDK sessions with many MCP tools get up to 7x faster tool rounds thanks to cached tool-pool assemblyARTIFACTS — Claude Code Artifacts turn session work into live, shareable web pages that update in place — useful for PR walkthroughs and dashboardsDEADLINE — Opus 4.7 fast mode is removed on July 24; speed: "fast" will error, so move to Opus 4.8 fast mode before thenMEMORY — Claude Code closes a cluster of long-session memory leaks, including MCP stdio stderr piling up to 64 MB per server and LSP documents staying open indefinitelyTABLES — Very large markdown tables no longer stall rendering; tables over 200 rows now show the first 200 with a "… N more rows" noticeSPEED — Sessions carrying many deny/ask permission rules no longer lose seconds every turn: rule matchers are compiled once and cachedTOOLS — print/SDK sessions with many MCP tools get up to 7x faster tool rounds thanks to cached tool-pool assemblyARTIFACTS — Claude Code Artifacts turn session work into live, shareable web pages that update in place — useful for PR walkthroughs and dashboardsDEADLINE — Opus 4.7 fast mode is removed on July 24; speed: "fast" will error, so move to Opus 4.8 fast mode before then
Articles/Claude Code
Claude Code/2026-07-16Advanced

The Permission Rules You Added for Safety Are Taxing Every Turn — Auditing the Ruleset Without Loosening It

Version 2.1.209 fixed the per-turn slowdown from large deny/ask rulesets, but the design debt in your rules is still yours. Here are the audit scripts, a shadowing detector, a turn-timing harness, and how to fold enumerated rules into prefix rules safely.

Claude Code193Permissionssettings.json2MCP44Performance3

Premium Article

I opened the settings.json for the publishing pipeline I run as an indie developer and found 140-plus lines in the deny array.

Every line had a story. A command that scared me during an overnight run. A path I did not want read by accident. A new "just in case" entry every time I connected another MCP server. Six months of that.

And every one of those sessions felt vaguely slow. I never found a cause, so I filed it under "the model is having a day."

The 2.1.209 release notes named it. Sessions with many deny/ask rules were losing seconds on every turn, and the fix was to compile the matchers once and cache them. The settings I had stacked up in the name of caution were quietly costing me speed.

What the fix removed, and what it left on your desk

Worth separating clearly: 2.1.209 removed a runtime recompilation cost. It did not remove your ruleset design debt.

CostAfter 2.1.209Whose problem
Compiling rule strings into matchers (per turn)Cached — goneClaude Code
Assembling the MCP tool pool (per round)Cached — up to 7x fasterClaude Code
The rule count itself (how much to match against)Still grows linearlyYou
Shadowed and contradictory rulesStill thereYou
How many MCP servers you attachStill growsYou

The top two arrive with an update. The bottom three stay until you deal with them.

There is a twist here. Now that the compile cost is gone, the pain that used to signal the problem is gone too. Debt grows fastest when it stops hurting, which is exactly why I wanted to count mine now.

Count first — auditing the ruleset

Deleting rules on a hunch is a bad idea, so I let a script do the counting. It reads your settings files and reports the rule count, duplicates, and distribution per tool.

#!/usr/bin/env python3
"""audit_permissions.py — inventory the permission rules in your settings files.
 
Usage:
    python3 audit_permissions.py ~/.claude/settings.json .claude/settings.json
"""
import json
import re
import sys
from collections import Counter, defaultdict
from pathlib import Path
 
BUCKETS = ("allow", "ask", "deny")
 
# "Bash(rm -rf:*)" -> ("Bash", "rm -rf:*") / "Read" -> ("Read", None)
RULE_RE = re.compile(r"^(?P<tool>[A-Za-z_][\w-]*)(?:\((?P<arg>.*)\))?$")
 
 
def parse_rule(rule: str):
    m = RULE_RE.match(rule.strip())
    if not m:
        return None, rule.strip()
    return m.group("tool"), m.group("arg")
 
 
def load(path: Path) -> dict:
    if not path.exists():
        print(f"  skip (not found): {path}")
        return {}
    with path.open(encoding="utf-8") as fh:
        return json.load(fh)
 
 
def audit(paths):
    total = Counter()
    by_tool = defaultdict(lambda: defaultdict(list))
    seen = defaultdict(list)  # rule -> [(bucket, file), ...]
 
    for p in paths:
        data = load(Path(p).expanduser())
        perms = data.get("permissions", {})
        for bucket in BUCKETS:
            for rule in perms.get(bucket, []):
                total[bucket] += 1
                tool, arg = parse_rule(rule)
                by_tool[tool][bucket].append(arg)
                seen[rule].append((bucket, str(p)))
 
    print("=== Totals ===")
    for b in BUCKETS:
        print(f"  {b:5s}: {total[b]:4d}")
    print(f"  total: {sum(total.values())}")
 
    print("\n=== Per tool (most rules first) ===")
    rows = sorted(by_tool.items(), key=lambda kv: -sum(len(v) for v in kv[1].values()))
    for tool, buckets in rows:
        counts = " ".join(f"{b}={len(buckets[b])}" for b in BUCKETS if buckets[b])
        print(f"  {tool:28s} {counts}")
 
    print("\n=== Exact duplicates and bucket conflicts ===")
    dup = 0
    for rule, hits in seen.items():
        if len(hits) < 2:
            continue
        dup += 1
        buckets = {b for b, _ in hits}
        mark = "CONFLICT" if len(buckets) > 1 else "dup"
        print(f"  [{mark}] {rule}")
        for b, f in hits:
            print(f"        {b:5s} <- {f}")
    if dup == 0:
        print("  none")
    return sum(total.values())
 
 
if __name__ == "__main__":
    args = sys.argv[1:] or ["~/.claude/settings.json", ".claude/settings.json"]
    n = audit(args)
    sys.exit(0 if n else 1)

On my machine it printed 141 deny, 22 ask, and 63 allow — 226 rules total. Nine exact duplicates, and two conflicts where a project setting put something in allow while my user setting denied it. Both conflicts resolved to the safe side, which is precisely why I had never noticed them.

Before running it I would have guessed around 60. Being off by an order of magnitude is a useful place to start.

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
An audit script that counts, dedupes, and surfaces bucket conflicts across your settings files
A turn-timing harness using claude -p, with the two details that make the comparison trustworthy
How to fold enumerated deny rules into prefix rules without opening a hole
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 $10 for lifetime access
View Membership →

Related Articles

Claude Code2026-07-16
Your Overnight Session Wakes Up at 3GB — Four Places Memory Piles Up, and How to Tell Them Apart
The Claude Code process I left running overnight had grown to 3.4GB of resident memory by morning. Here are the four accumulation sources closed in 2.1.209, how to separate what's left in your own setup by sampling RSS slope, and a watchdog pattern that folds a session before it hurts.
Claude Code2026-06-28
When You Fan Out Streaming Sessions, Your Laptop's CPU Gives Out First — An Adaptive Throttle That Caps Concurrency by Measured Load
Even with lighter streaming, fanning out many sessions on one machine saturates the host CPU before anything else. Here is why a fixed semaphore fails, plus a working adaptive gate that raises and lowers concurrency from measured CPU.
Claude Code2026-06-27
When an OAuth Token Expires, Your Unattended Run Has Nowhere to Go — A Token-Lifecycle Design That Keeps Remote MCP Alive
Remote MCP connectors are authorized via OAuth, but access tokens are short-lived. Interactive sessions can re-authorize in a browser; an unattended scheduled run has nobody to click the dialog. Here is a token-lifecycle design that owns expiry and refreshes ahead of time.
📚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
See all →