Skip to content

Resolve the nearest existing folder in the hint guard - #86

Merged
gabe4coding merged 4 commits into
mainfrom
harness/ccm-guard-nearest-folder
Oct 27, 2026
Merged

gabe4coding merged 4 commits into
mainfrom
harness/ccm-guard-nearest-folder

Conversation

@gabe-bots

@gabe-bots gabe-bots Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Intent

The hint guard resolves the nearest existing folder for paths that create new folders.
A write makes the missing folders of its path, so the nearest folder that exists decides where the file lands.
The guard now takes the real path of that folder and adds the rest of the path.
It does this for the path of the call and for each guarded folder.

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["The model calls Write, Edit or NotebookEdit"] --> B["Find the nearest folder of the path that exists"]
  B --> C["Take its real path and add the rest of the path"]
  C --> D["Do the same for each guarded folder"]
  D --> E{"Is the path inside a guarded folder?"}
  E -->|yes| F["Deny the call with the guard message"]
  E -->|no| G["Run the file tool"]
Loading

Drift

None.

Decisions

  • Before, the guard resolved only the file and its parent folder. When the parent folder did not exist yet, the path stayed as written.
  • The new step walks up the path until a folder exists. A path has few parts, so the number of stat calls stays small.
  • The guarded folders get the same step. HOME or the project can be a symbolic link, and a hint folder often does not exist yet.
  • The guard keeps the path as written as well, so each case that it denied before, it denies now.
  • The step does not follow a link that points to a path that does not exist. The engine gives no readlink.
  • Base: main. The change does not need the letter-case change of Guard hint folders in any letter case #83, and the two work together.
  • Version 0.12.18. Guard hint folders in any letter case #83 also sets 0.12.18, so the second of the two to merge needs 0.12.19.
  • Door: two-way. A revert restores the step with only the parent folder.

Evidence

Head 319ba53 on main 548fddb. Tree c3fe0023584429910799e977e736727fe980ead3.

Real Claude Code 2.1.296 on Linux, with the plugin from a local git copy of the commit, in the permission mode that asks for no approval.
A scripted Messages API on localhost sends the Write calls, because the box has no Claude Code login. Claude Code, the plugin hooks, the Write tool and the file system are real.
The project has symbolic links to guarded folders, and the target folders do not exist yet. A second run sets HOME to a symbolic link.

Case main 548fddb (0.12.17) this branch (0.12.18)
3 writes through a link into new folders under a guarded folder created denied
1 write by the real path of a linked HOME into a missing hint folder created denied
3 writes into guarded folders that exist (controls) denied denied
1 write outside the guarded folders (control) created created

The new tests on main, then on this branch:

main:   249 pass, 2 fail   (guards a path through a link whose next folders do not exist yet; guards the folders by their real path when HOME is a link)
branch: 251 pass, 0 fail

runner.integration, docs.check, code.check, tsc -p tsconfig.pure.json, format.sh --check, version.check origin/main and claude plugin validate . exit 0.
A trial merge with #83 (docs sentence merged, version 0.12.19) passes 260 tests, and the same Claude Code runs give the same "After" lines.

Reading map

  • Tests changed: tests/hints.test.ts lines 1606-1662 add a link model for fs.stat and three tests. No test is removed or weakened.
  • Config: .claude-plugin/plugin.json line 3, the version.
  • Public API: none. hooks/register.tsx lines 792-822: realFormOf, and landings and guardedHintDirs use it.
  • Docs: docs/hints.mdx lines 230-232.

A write makes the missing folders of its path, so the folder that decides
where it lands is the nearest one that exists. The guard now takes the real
path of that folder, then adds the rest of the path. It does this for the
path of the call and for each guarded folder, so a symbolic link in HOME, in
the project or in a path to a new folder gives the same result.

Version 0.12.18.
@gabe-bots gabe-bots Bot added the needs-gabe Needs Gabe's decision before work continues label Oct 27, 2026
@gabe-bots

gabe-bots Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

@greptile review

1 similar comment

Copy link
Copy Markdown
Owner

@greptile review

@greptile-apps

greptile-apps Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 3/5

[Medium impact] The change affects file-tool calls that target guarded hint, function, or installed-plugin folders.

Restore the resolved-parent check before merging so existing file links cannot escape the hint-file guard.

Findings

  1. P1 Security Linked hint files escape denial ▶
Fix with agent prompt
### Issue 1
hooks/register.tsx:820-821
`landings` drops the resolved parent when an existing file is a symbolic link. For example, let `/repo/cm` link to `$HOME/.claude/code-mode`, and let its `hints/x.md` link to `/tmp/x.md`. A call on `/repo/cm/hints/x.md` now produces only the written path and `/tmp/x.md`. Neither matches a guarded folder, so the call passes. Previously, the resolved parent added `$HOME/.claude/code-mode/hints/x.md` and denied it.

Keep the resolved-parent candidate alongside the new ancestor check, and add an existing-file-link test. This also satisfies the instruction in `CODING_STANDARDS.md`: “Never weaken … the guard of hint and function files.”

**How this was verified:** For this link layout, the early return resolves the file outside the guarded folder, and neither remaining candidate names its protected parent.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR resolves the nearest existing ancestor for file-tool paths and guarded folders.

  • The file-tool guard follows links through the nearest existing folder.

Reviews (1) · Last reviewed commit: "Resolve the nearest existing folder in t..." · Reviewed by Greptile

Comment thread hooks/register.tsx Outdated
Comment on lines +820 to +821
const real = await realFormOf($, full)
return real === undefined ? [full] : [full, real]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Linked hint files escape denial

landings drops the resolved parent when an existing file is a symbolic link. For example, let /repo/cm link to $HOME/.claude/code-mode, and let its hints/x.md link to /tmp/x.md. A call on /repo/cm/hints/x.md now produces only the written path and /tmp/x.md. Neither matches a guarded folder, so the call passes. Previously, the resolved parent added $HOME/.claude/code-mode/hints/x.md and denied it.

Keep the resolved-parent candidate alongside the new ancestor check, and add an existing-file-link test. This also satisfies the instruction in CODING_STANDARDS.md: “Never weaken … the guard of hint and function files.”

How this was verified: For this link layout, the early return resolves the file outside the guarded folder, and neither remaining candidate names its protected parent.

Context Used: AGENTS.md (source)

Knowledge Base Used: Session startup and paths

Prompt To Fix With AI
This is a comment left during a code review.
Path: hooks/register.tsx
Line: 820-821

Comment:
**Linked hint files escape denial**

`landings` drops the resolved parent when an existing file is a symbolic link. For example, let `/repo/cm` link to `$HOME/.claude/code-mode`, and let its `hints/x.md` link to `/tmp/x.md`. A call on `/repo/cm/hints/x.md` now produces only the written path and `/tmp/x.md`. Neither matches a guarded folder, so the call passes. Previously, the resolved parent added `$HOME/.claude/code-mode/hints/x.md` and denied it.

Keep the resolved-parent candidate alongside the new ancestor check, and add an existing-file-link test. This also satisfies the instruction in `CODING_STANDARDS.md`: “Never weaken … the guard of hint and function files.”

**How this was verified:** For this link layout, the early return resolves the file outside the guarded folder, and neither remaining candidate names its protected parent.

**Context Used:** AGENTS.md ([source](https://github.com/gabe4coding/claude-code-mode/blob/main/AGENTS.md))

**Knowledge Base Used:** [Session startup and paths](https://app.greptile.com/gabe4coding/-/custom-context/knowledge-base/gabe4coding/claude-code-mode/-/docs/session-startup-and-paths.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

A hint file can itself be a symbolic link out of its folder. A write by
a linked folder to that file then names a file in the hint folder, but the
real form of the file is outside it. The guard now keeps the real form of
the parent folder with the file name as one more place where the path can
land, as it did before the nearest folder step. It also keeps the path as
written and the real form of the file.

Gabriele Pavanello
gabry.pavanello@gmail.com
@gabe4coding
gabe4coding merged commit 2bce867 into main Oct 27, 2026
13 checks passed
@gabe4coding
gabe4coding deleted the harness/ccm-guard-nearest-folder branch October 27, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-gabe Needs Gabe's decision before work continues risk:t2-soft

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants