---
title: "Treat every link to the app's own scheme · Agent Wranglers"
url: https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/app-scheme-links
id: DSK-30
level: Required
risk: takeover
updated: 2026-10-22
build: a2a6bdfed830
tokens: 1256
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Treat every link to the app's own scheme as untrusted input

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Web code inside desktop apps](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: DSK-30
- Level: Required
- Chapter: [Web code inside desktop apps](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps.md)
- Rule: Parse each link to the app's custom scheme, such as `ledgerly://`, against a fixed list of actions and argument shapes. Ask the user before an action that changes data, and never pass link text to a shell, a file path or a command-line flag.
- Check: A test opens the app with `ledgerly://open?doc=../../etc/passwd`, with `ledgerly://x" --inspect=9229`, with a 100,000 character link and with an unknown action. Each is refused with no side effect, and no debug port opens. A valid link that deletes data shows a confirmation first.
- Risk: An account or system taken over
- How: Any web page or email can open such a link, so its sender is a stranger. On Windows the link arrives as a command-line argument. Microsoft warns that extra quotes or backslashes in it can add arguments of the attacker's choice. Quote `"%1"` in the registration, and refuse a launch that holds any argument besides the one link. Electron delivers the link through `open-url` on macOS and `second-instance` on Windows and Linux, so handle both with one parser. Cap the length and test with long links and odd characters, as Microsoft advises. Hand any web address inside the link to [Open outside links only after checking the address](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/safe-external-links.md). Check each action as [Check the sender and content of every message to the main process](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/validate-ipc.md) does.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
const MAX_LINK_LENGTH = 2048;
const ARGUMENT_SHAPES = new Map([['open', /^[a-z0-9-]{1,64}$/]]);

export function parseAppLink(raw) {
  if (typeof raw !== 'string' || raw.length > MAX_LINK_LENGTH || !URL.canParse(raw)) return null;
  const url = new URL(raw);
  const shape = ARGUMENT_SHAPES.get(url.hostname);
  const doc = url.searchParams.get('doc') ?? '';
  return url.protocol === 'ledgerly:' && shape?.test(doc) ? { action: url.hostname, doc } : null;
}

app.on('open-url', (event, url) => { event.preventDefault(); handleLink(parseAppLink(url)); });
app.on('second-instance', (_, commandLine) => handleLink(parseAppLink(commandLine.at(-1))));
```

### Plain text example

```csharp
const int MaxLinkLength = 2048;
static readonly Regex DocShape = new("^[a-z0-9-]{1,64}$");

static AppLink? ParseAppLink(string[] args)
{
    if (args.Length != 1 || args[0].Length > MaxLinkLength) return null;
    if (!Uri.TryCreate(args[0], UriKind.Absolute, out var uri) || uri.Scheme != "ledgerly") return null;
    var doc = HttpUtility.ParseQueryString(uri.Query)["doc"] ?? "";
    return uri.Host == "open" && DocShape.IsMatch(doc) ? new AppLink("open", doc) : null;
}
```

## Sources

- [Microsoft, Registering an application to a URI scheme](https://learn.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914%28v=vs.85%29)
- [Electron, Deep links](https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app)

## Related rules

- DSK-12 [Open outside links only after checking the address](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/safe-external-links.md) in Desktop apps and extensions
- DSK-11 [Check the sender and content of every message to the main process](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/validate-ipc.md) in Desktop apps and extensions

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/app-scheme-links">Agent Wranglers desktop apps and extensions playbook</a>`. In code you write, add a comment next to it with the same link.

- Previous: [Keep tokens in the system keychain](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/desktop-keychain.md)
- Next: [Turn off the Electron fuses the app does not use](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/electron-fuses.md)
- [Web code inside desktop apps](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps.md)
