fix(capabilities): give open_path a scope so it can actually run - #403
Merged
Conversation
`opener:allow-open-path` grants the command but carries no scope, and `allow-default-urls` - which has no commands, so its scope lands in the plugin's global scope and applies to every opener command - contains only URL entries. In tauri-plugin-opener, `Entry::Url::matches_path_program` returns false unconditionally, and the fs allow-list is empty, so `is_path_allowed` is false on both conjuncts. Every `open_path` call returns ForbiddenPath. The one place that calls it today is the "Open exported HTML file now?" prompt, whose Yes button has therefore never worked; it reports the failure as a toast rather than doing nothing, which is likely why it read as an ordinary error. `**` matches the width the app already has: `assetProtocol.scope` is `["**"]`, and the file commands take arbitrary paths from the frontend with no scope check, so read and write access to the filesystem is already unrestricted. The genuine increment is that `open_path` hands a path to the OS default handler, which for an executable means launching it - and that matters at the call site that takes untrusted input, not in the ACL. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
pushed a commit
that referenced
this pull request
Aug 2, 2026
`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and `anchor.href` is what the DOM resolved against the webview origin, not a path on disk. The two platforms then failed differently: on macOS and Linux the origin is `tauri://localhost`, which the opener scope refuses, so the click did nothing and left an uncaught promise rejection; on Windows it is `http://tauri.localhost`, which matches `http://*`, so the browser really opened onto a dead link. `resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for the whole scheme, drive-letter, UNC and query-suffix decision table, and adds two rules a link needs that an image does not: `//host/path` is a web address, matching what `getMarkdownLinkTarget` already assumes, and a relative link in an unsaved buffer resolves to nothing rather than to something relative to the process working directory. Both OS calls are now inside try/catch with a toast, which removes the uncaught rejection and makes the macOS failure visible. The file still will not open until the opener path scope is decided (#399, #403); everything above is an improvement regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
pushed a commit
that referenced
this pull request
Aug 2, 2026
`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and `anchor.href` is what the DOM resolved against the webview origin, not a path on disk. The two platforms then failed differently: on macOS and Linux the origin is `tauri://localhost`, which the opener scope refuses, so the click did nothing and left an uncaught promise rejection; on Windows it is `http://tauri.localhost`, which matches `http://*`, so the browser really opened onto a dead link. `resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for the whole scheme, drive-letter, UNC and query-suffix decision table, and adds two rules a link needs that an image does not: `//host/path` is a web address, matching what `getMarkdownLinkTarget` already assumes, and a relative link in an unsaved buffer resolves to nothing rather than to something relative to the process working directory. Both OS calls are now inside try/catch with a toast, which removes the uncaught rejection and makes the macOS failure visible. The file still will not open until the opener path scope is decided (#399, #403); everything above is an improvement regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
pushed a commit
that referenced
this pull request
Aug 2, 2026
`[data](./data.csv)` was handed to `openUrl(anchor.href)`, and `anchor.href` is what the DOM resolved against the webview origin, not a path on disk. The two platforms then failed differently: on macOS and Linux the origin is `tauri://localhost`, which the opener scope refuses, so the click did nothing and left an uncaught promise rejection; on Windows it is `http://tauri.localhost`, which matches `http://*`, so the browser really opened onto a dead link. `resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for the whole scheme, drive-letter, UNC and query-suffix decision table, and adds two rules a link needs that an image does not: `//host/path` is a web address, matching what `getMarkdownLinkTarget` already assumes, and a relative link in an unsaved buffer resolves to nothing rather than to something relative to the process working directory. Both OS calls are now inside try/catch with a toast, which removes the uncaught rejection and makes the macOS failure visible. The file still will not open until the opener path scope is decided (#399, #403); everything above is an improvement regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
added a commit
that referenced
this pull request
Aug 3, 2026
…ent (#409) `[data](./data.csv)` was handed to `openUrl(anchor.href)`, and `anchor.href` is what the DOM resolved against the webview origin, not a path on disk. The two platforms then failed differently: on macOS and Linux the origin is `tauri://localhost`, which the opener scope refuses, so the click did nothing and left an uncaught promise rejection; on Windows it is `http://tauri.localhost`, which matches `http://*`, so the browser really opened onto a dead link. `resolveLocalFileLinkPath` reuses `resolveExportImagePath` from #363 for the whole scheme, drive-letter, UNC and query-suffix decision table, and adds two rules a link needs that an image does not: `//host/path` is a web address, matching what `getMarkdownLinkTarget` already assumes, and a relative link in an unsaved buffer resolves to nothing rather than to something relative to the process working directory. Both OS calls are now inside try/catch with a toast, which removes the uncaught rejection and makes the macOS failure visible. The file still will not open until the opener path scope is decided (#399, #403); everything above is an improvement regardless. Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PathGao
added a commit
that referenced
this pull request
Aug 3, 2026
… asset URL (#415) `resolveLocalFileLinkPath` delegates to `resolveExportImagePath`, which accepts `asset://localhost/…` and `http://asset.localhost/…` - the shape a local image's `src` takes inside the webview, which the exporter has to turn back into a disk path to inline the bytes. A link is the opposite situation: the href is the author's own text. So `[report](http://asset.localhost/Users/me/.ssh/id_rsa)` reads as a remote address in the link text and the status bar, resolves to `/Users/me/.ssh/id_rsa`, and is handed to the OS default handler - now reachable, since the opener path scope was opened in #403. Reusing the image resolver was right; inheriting that one clause of it was not. The link resolver rejects asset URLs before delegating. Co-authored-by: PathGao <gaoyanbo@gaoyanbodeMacBook-Air.local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #399.
The defect
capabilities/default.jsonlistsopener:allow-open-path, which grants the command. Scope is resolved separately, and there it fails:allow-open-path["open_path"]nullallow-default-urls[][mailto:*, tel:*, http://*, https://*]A permission with no commands lands in the plugin's global scope, applying to every opener command — including
open_path. And intauri-plugin-opener2.5.3:is_path_allowed = fs_scope.is_allowed(path) && allowed.any(matches_path_program)— the fs allow-list is empty, so the first conjunct is false; every scope entry is aUrl, so the second is too.open_pathreturnsForbiddenPathon every platform in every window.What it actually breaks today
One call site: the "Open exported HTML file now?" prompt after an HTML export. PDF export never offers it, and there is no auto-open anywhere — this is an opt-in yes/no dialog whose Yes button has never worked.
It is not silent: the call site catches the rejection and shows
toast.openExportedFileFailed. So it has read as an ordinary failure rather than a missing capability, which is probably why it went unreported.Why
**It matches the width the app already has, rather than adding to it:
assetProtocol.scopeis already["**"]— the webview can read any file throughasset://.read_file_content,save_file_content,copy_file,delete_fileall take arbitrary paths from the frontend with no scope check.So unrestricted read and write are already the status quo. The genuine increment is different in kind:
open_pathhands a path to the OS default handler, and for an executable that means launching it.That increment belongs to the call site, not to the ACL. The two call sites differ:
askToOpenExportedFileNarrowing the ACL would block the first — which has no untrusted input at all — while the second is better answered where the path is produced. A follow-up PR adds that second call site; the question of what a document should be allowed to ask the OS to open belongs in its review, and I'll raise it there.
If you would rather have a narrower scope anyway, say so and I'll write it. Note it cannot be a static list: the export destination is wherever the user's save dialog pointed, so it needs the plugin's runtime scope API rather than a
capabilitiesentry.Verification
cargo buildregeneratesgen/schemas/capabilities.json, which now carries:{ "identifier": "opener:allow-open-path", "allow": [{ "path": "**" }] }Build clean. No behaviour change beyond the one command becoming callable — no test asserts the old rejection, and nothing else in the tree calls
open_path.🤖 Generated with Claude Code