Skip to content

[Bug]: permission patterns anchored with // or ~/ never match — ctx_execute_file blocks allowed paths and the deny gate fails open #1075

Closed

Description

@madonoharu

TL;DR — parseToolPattern passes the text inside Read(...) to the glob converter as-is, so the two anchors Claude Code documents as absolute never match anything. Two consequences: ctx_execute_file blocks paths the user explicitly allowed, and the deny gate silently lets through paths the user explicitly denied. Only the /path form matches, and that is the one form Claude Code does not resolve against the filesystem root. Minimal fix is to normalize // and ~/ before globbing.

Platform

Claude Code

context-mode version

1.0.169

Debug script output (REQUIRED)

The four failing checks in the report below are FTS5 in-memory test, Session DB uses WAL mode, No orphaned -shm files, and Concurrent SQLite writes (3 conns × 30). None of them touch this bug: the reproduction calls the shipped permission matcher directly and goes nowhere near SQLite.

ctx-debug.sh output (config file contents redacted, username and hostname replaced)
{
  "version": "2.0.0",
  "generated": "2026-08-24T08:54:57Z",
  "sections": {
    "1. System Info": {
      "checks": [],
      "info": {
        "OS type": "macos",
        "uname -a": "Darwin HOSTNAME 25.6.0 Darwin Kernel Version 25.6.0: Fri Jul 31 19:16:36 PDT 2026; root:xnu-12377.161.14~5/RELEASE_ARM64_T6030 arm64",
        "Architecture": "arm64",
        "macOS version": "26.6.2 (25G83)",
        "Shell": "/bin/zsh",
        "Bash version": "3.2.57(1)-release",
        "Locale": "LANG=ja_JP.UTF-8, LC_ALL=unset"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "2. Runtime Versions": {
      "checks": [],
      "info": {
        "Node.js": "v26.5.0",
        "node path": "/Users/USER/.local/share/mise/installs/node/26.5.0/bin/node",
        "execPath": "/Users/USER/.local/share/mise/installs/node/26.5.0/bin/node",
        "Node install method": "unknown",
        "Bun": "1.4.0",
        "bun path": "/Users/USER/.local/share/mise/installs/bun/latest/bin/bun",
        "Codex CLI": "codex-cli 0.149.1",
        "Python": "Python 3.14.7",
        "Ruby": "ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin25]",
        "npm": "12.0.2",
        "npm global root": "/Users/USER/.local/share/mise/installs/node/26.5.0/lib/node_modules"
      },
      "configs": [],
      "warnings": [
        "Codex ≥0.118.0: exec-mode MCP broken (openai/codex#16685). Pin to ≤0.116.0 for exec-mode."
      ],
      "details": []
    },
    "3. context-mode Installation": {
      "checks": [
        {
          "pass": true,
          "label": "build/ directory exists"
        },
        {
          "pass": true,
          "label": "hooks/pretooluse.mjs exists"
        },
        {
          "pass": true,
          "label": "hooks/sessionstart.mjs exists"
        },
        {
          "pass": true,
          "label": "server.bundle.mjs exists"
        },
        {
          "pass": true,
          "label": "cli.bundle.mjs exists"
        }
      ],
      "info": {
        "Installed version": "1.0.169",
        "Plugin root": "~/.claude/plugins/cache/context-mode/context-mode/1.0.169",
        "Install method": "marketplace",
        "npm latest": "1.0.169"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "4. better-sqlite3 Native Module": {
      "checks": [
        {
          "pass": true,
          "label": "better-sqlite3 .node binary exists"
        },
        {
          "pass": true,
          "label": "require('better-sqlite3') succeeds"
        },
        {
          "pass": true,
          "label": "npm ignore-scripts is false"
        }
      ],
      "info": {
        "Binary path": "~/.claude/plugins/cache/context-mode/context-mode/1.0.169/node_modules/better-sqlite3/build/Release/better_sqlite3.abi147.node",
        "Node ABI version": "147"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "5. Adapter Detection": {
      "checks": [],
      "info": {
        "Active adapter (env)": "none",
        "Installed adapters": "claude-code codex gemini-cli"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "6. Config Files": {
      "checks": [],
      "info": {
        "Claude settings.json": "~/.claude/settings.json (exists)",
        "Claude settings.local.json": "~/.claude/settings.local.json (not found)",
        "Gemini settings.json": "~/.gemini/settings.json (exists)",
        "Codex config.toml": "~/.codex/config.toml (exists)",
        "Codex hooks.json": "~/.codex/hooks.json (exists)",
        "Cursor hooks.json (project)": "~/.claude/.cursor/hooks.json (not found)",
        "Cursor mcp.json (project)": "~/.claude/.cursor/mcp.json (not found)",
        "Cursor mcp.json (global)": "~/.cursor/mcp.json (not found)",
        "Cursor hooks.json (global)": "~/.cursor/hooks.json (not found)",
        "VS Code mcp.json (project)": "~/.claude/.vscode/mcp.json (not found)",
        "opencode.json (project)": "~/.claude/opencode.json (not found)",
        "opencode.json (dotdir)": "~/.claude/.opencode/opencode.json (not found)",
        "opencode.json (global)": "~/.config/opencode/opencode.json (not found)",
        "KiloCode settings.json": "~/.config/kilo/settings.json (not found)",
        "OpenClaw openclaw.json": "~/.openclaw/openclaw.json (not found)",
        "Kiro mcp.json": "~/.kiro/settings/mcp.json (not found)",
        "Kiro default agent": "~/.kiro/agents/default.json (not found)",
        "Zed settings.json": "~/.config/zed/settings.json (not found)"
      },
      "configs": [
        {
          "name": "Claude settings.json",
          "path": "~/.claude/settings.json",
          "exists": true,
          "content": "[redacted]"
        },
        {
          "name": "Gemini settings.json",
          "path": "~/.gemini/settings.json",
          "exists": true,
          "content": "[redacted]"
        },
        {
          "name": "Codex config.toml",
          "path": "~/.codex/config.toml",
          "exists": true,
          "content": "[redacted]"
        },
        {
          "name": "Codex hooks.json",
          "path": "~/.codex/hooks.json",
          "exists": true,
          "content": "[redacted]"
        }
      ],
      "warnings": [],
      "details": []
    },
    "7. Hook Validation": {
      "checks": [
        {
          "pass": true,
          "label": "No stale hook paths in Claude settings.json"
        },
        {
          "pass": true,
          "label": "PreToolUse registered"
        },
        {
          "pass": true,
          "label": "PostToolUse registered"
        },
        {
          "pass": true,
          "label": "PreCompact registered"
        },
        {
          "pass": true,
          "label": "SessionStart registered"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    },
    "8. SQLite / FTS5 Test": {
      "checks": [
        {
          "pass": false,
          "label": "FTS5 in-memory test"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    },
    "9. Executor Test": {
      "checks": [
        {
          "pass": true,
          "label": "node subprocess spawn"
        },
        {
          "pass": true,
          "label": "bash subprocess spawn"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    },
    "10. Process Check": {
      "checks": [],
      "info": {
        "Running context-mode processes": "7"
      },
      "configs": [],
      "warnings": [
        "Multiple context-mode processes — possible orphans"
      ],
      "details": []
    },
    "11. Session Databases": {
      "checks": [],
      "info": {
        "~/.claude/context-mode/sessions": "143 files,  90M",
        "Total session DBs": "143"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "12. Environment Variables": {
      "checks": [],
      "info": {
        "TMPDIR": "/var/folders/hk/dlx6twc54jq9c_zw10n5hdq40000gp/T/",
        "TEMP": "unset",
        "TMP": "unset",
        "NODE_OPTIONS": "--require=/var/folders/hk/dlx6twc54jq9c_zw10n5hdq40000gp/T/cmux-claude-node-options/restore-node-options.cjs --max-old-space-size=4096",
        "NODE_PATH": "/Users/USER/.claude/plugins/cache/context-mode/context-mode/1.0.169/node_modules:",
        "NODE_EXTRA_CA_CERTS": "/Users/USER/ca/Cloudflare_CA.pem",
        "SSL_CERT_FILE": "/Library/Python/3.9/site-packages/certifi/cacert.pem",
        "CURL_CA_BUNDLE": "unset",
        "SSH_AUTH_SOCK": "yes (set)",
        "CONTEXT_MODE_SESSION_SUFFIX": "unset",
        "CONTEXT_MODE_NODE": "unset"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "13. Hook Execution": {
      "checks": [
        {
          "pass": true,
          "label": "PreToolUse denies WebFetch"
        },
        {
          "pass": true,
          "label": "PreToolUse passes Write tool through"
        },
        {
          "pass": true,
          "label": "SessionStart injects routing context"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    },
    "14. MCP Server Startup": {
      "checks": [
        {
          "pass": true,
          "label": "Server entry + deps resolve"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    },
    "15. SQLite Concurrency": {
      "checks": [
        {
          "pass": false,
          "label": "Session DB uses WAL mode"
        },
        {
          "pass": true,
          "label": "No oversized WAL journals (>1MB)"
        },
        {
          "pass": false,
          "label": "No orphaned -shm files"
        },
        {
          "pass": false,
          "label": "Concurrent SQLite writes (3 conns × 30)"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    },
    "16. Adapter Validation": {
      "checks": [
        {
          "pass": true,
          "label": "Adapter validation passes"
        }
      ],
      "info": {
        "Validated adapter": "Claude Code"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "17. Sandbox Environment": {
      "checks": [
        {
          "pass": true,
          "label": "System CA certificates found"
        },
        {
          "pass": true,
          "label": "Temp dir writable"
        },
        {
          "pass": true,
          "label": "Env denylist strips dangerous vars"
        }
      ],
      "info": {
        "Effective temp dir": "/var/folders/hk/dlx6twc54jq9c_zw10n5hdq40000gp/T/"
      },
      "configs": [],
      "warnings": [],
      "details": []
    },
    "18. Network / TLS": {
      "checks": [
        {
          "pass": true,
          "label": "NODE_EXTRA_CA_CERTS file exists"
        },
        {
          "pass": true,
          "label": "HTTPS to npm registry"
        },
        {
          "pass": true,
          "label": "Temp dir >100MB free"
        }
      ],
      "info": {},
      "configs": [],
      "warnings": [],
      "details": []
    }
  },
  "summary": {
    "pass": 27,
    "fail": 4,
    "warnings": 2,
    "total": 31
  }
}

Exact prompt that triggered the bug (REQUIRED)

Count the lines of the scan script I just wrote in the scratchpad directory.

Full error output (REQUIRED)

File access blocked: "/private/tmp/claude-UID/<session>/scratchpad/scan.mjs" resolves outside the
project root (/Users/USER/.claude). context-mode confines ctx_execute_file to the workspace so it
cannot be used to bypass the host's sandbox/permission controls (issue #852). To intentionally
process a file outside the project, add a host allow rule, e.g. "permissions": { "allow":
["Read(/private/tmp/claude-UID/<session>/scratchpad/scan.mjs)"] } in your settings.

The rule that message suggests uses a single leading slash, which Claude Code resolves against the settings source rather than the filesystem root — so following the advice verbatim produces a rule that grants nothing on the host side.

Steps to reproduce (REQUIRED)

Allow side, through the tool. This is the path I hit.

  1. Add an absolute-path allow rule to ~/.claude/settings.json, in the form Claude Code writes and documents:

    { "permissions": { "allow": ["Read(//private/tmp/**)"] } }
  2. From a project rooted somewhere else, call ctx_execute_file on a file under /private/tmp/.

  3. The call is blocked with the message above, even though the rule covers the path.

Both sides, against the matcher directly. Run these from the installed plugin root, or from a checkout after npm run build:

$ cd ~/.claude/plugins/cache/context-mode/context-mode/1.0.169
$ node --input-type=module -e "
import { evaluateProjectContainment as e } from './build/security.js';
// (path, projectRoot, allowGlobs) — allowGlobs is one array per settings file
const p = '/Users/alice/notes/todo.md', root = '/Users/alice/some-project';
for (const g of ['//Users/alice/notes/**', '~/notes/**', '/Users/alice/notes/**'])
  console.log(JSON.stringify(g), '=>', JSON.stringify(e(p, root, [[g]])));
"
"//Users/alice/notes/**" => {"allowed":false,"reason":"outside"}
"~/notes/**" => {"allowed":false,"reason":"outside"}
"/Users/alice/notes/**" => {"allowed":true,"reason":"allow-rule"}
$ node --input-type=module -e "
import { evaluateFilePath as f } from './build/security.js';
// (path, denyGlobs, caseInsensitive, projectRoot) — denied:true means a deny rule matched
const p = '/Users/alice/project/.env';
for (const g of ['//Users/alice/project/**', '~/project/**', '/Users/alice/project/**', '**/.env'])
  console.log(JSON.stringify(g), '=>', JSON.stringify(f(p, [[g]], true, '/Users/alice/project')));
"
"//Users/alice/project/**" => {"denied":false}
"~/project/**" => {"denied":false}
"/Users/alice/project/**" => {"denied":true,"matchedPattern":"/Users/alice/project/**"}
"**/.env" => {"denied":true,"matchedPattern":"**/.env"}

The second run is the one that matters: a user who writes Read(//Users/alice/secrets/**) under permissions.deny gets no protection from the deny gate. Relative patterns such as **/.env still match, so the gate is not broken across the board — only for the two absolute anchors.

Deny side, from a settings file. readToolDenyPatterns takes an explicit globalSettingsPath, so the read-parse-match chain can be driven without touching real settings. With {"permissions": {"deny": ["Read(//Users/alice/project/secret.txt)"]}} in a scratch file:

$ node --input-type=module -e "
import { readToolDenyPatterns, evaluateFilePath } from './build/security.js';
const g = readToolDenyPatterns('Read', undefined, '/tmp/fake-settings.json');
console.log('parsed deny globs:', JSON.stringify(g));
console.log('verdict:', JSON.stringify(evaluateFilePath('/Users/alice/project/secret.txt', g, true, '/Users/alice/project')));
"
parsed deny globs: [["//Users/alice/project/secret.txt"]]
verdict: {"denied":false}

The rule is read and parsed correctly, and then matches nothing. The only hop I did not execute is src/server.ts:1220-1227 calling these two functions, which is visible in the source.

What have you tried to fix it?

The host is not at fault here. Claude Code's permission documentation defines the anchors:

Pattern Meaning Example
//path Absolute path from filesystem root Read(//Users/alice/secrets/**)
~/path Path from home directory Read(~/Documents/*.pdf)

It warns explicitly: "A pattern like /Users/alice/file isn't an absolute path. The single leading slash anchors at the settings source, not the filesystem root. Use //Users/alice/file for absolute paths." For deny rules it steers users to the same forms: "To write a rule in user settings that applies inside every project, use a // absolute path or a ~/ home-relative path instead."

Where it comes from. parseToolPattern (src/security.ts:37-44) returns the raw text between the parentheses. readToolPermissionPatterns (src/security.ts:426, pushed at 455-457) collects it, evaluateFilePath hands it to fileGlobToRegex (src/security.ts:101-135, called at 646), and that converter escapes everything except *, **, and ?. Nothing strips the extra leading slash and nothing expands ~ — homedir does not appear in src/security.ts at all. So //private/tmp/** can only ever match a path that literally begins with two slashes.

Both consumers inherit it: the allow-rule escape hatch in evaluateProjectContainment (src/security.ts:766-783, reached from src/server.ts:1189-1192) and the deny gate (readToolDenyPatterns at src/security.ts:406-412, reached from src/server.ts:1220-1227). Line numbers are from the 1.0.169 tag; src/security.ts is identical on next, src/server.ts is not.

Workaround, allow side. Add a second rule in the single-slash form next to the documented one:

"Read(/private/tmp/claude-*/**/scratchpad/**)"

This matcher accepts that entry. On the Claude Code side it resolves to ~/.claude/private/tmp/..., so it grants nothing and exists only to satisfy context-mode. Verified here: after adding it, the same ctx_execute_file call succeeds, with no restart.

Workaround, deny side. Rewrite any // or ~/ deny rule you rely on as a relative pattern — Read(**/secrets/**) rather than Read(//Users/alice/secrets/**). Relative patterns still match, as the run above shows.

Suggested fix, minimal. Normalize the anchor once, where the pattern is read:

  • //path — drop one leading slash and match from the filesystem root
  • ~/path — expand against homedir()

That restores both the allow escape hatch and the deny gate.

Separate, larger. Resolving a single leading / against the directory of the settings file the rule came from, as the docs describe, needs the pattern's provenance threaded through readToolPermissionPatterns. Worth doing, but it is a different change and the two above stand on their own.

Pre-submission checklist

  • I have run the debug script and pasted the output above
  • I am using the latest version of context-mode
  • I have searched existing issues for duplicates
  • I have included steps to reproduce the issue

Operating System

macOS (Darwin 25.6.0)

JS Runtime

node v26.5.0 / bun 1.4.0

Activity

  1. added 2 commits that reference this issue on Oct 12, 2026
    2f15a2c
    6eebf17
  2. mksglu commented on Nov 4, 2026

    @mksglu
    Owner

    2.0 understands the ~/ and //abs anchors in your deny rules. A deny for Read(~/secret/**) or Read(//etc/**) now blocks the matching file, and tests cover both.

    Context Mode 2.0 is out. To try it in Claude Code:

    claude plugin marketplace add mksglu/context-mode
    claude plugin install context-mode@context-mode

    Please test it, and reopen this issue or open a new one with your ctx_doctor output if it still happens: https://github.com/mksglu/context-mode/issues/new?template=agent-report.yml

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions