Troubleshooting

When a link opens in the wrong place, Grinch can tell you which rule sent it there and why.

A link opened in the wrong place

Work through these in order. The commands assume the Grinch alias from Get started.

1. Test the URL

--test runs a URL through your rules without opening a browser and prints the rewritten URL, the target, and any launch arguments:

Terminal
Grinch --test "https://github.com/jamtur01/grinch?utm_source=slack"
URL:     https://github.com/jamtur01/grinch?utm_source=slack
Final:   https://github.com/jamtur01/grinch
Browser: com.google.Chrome
Args:    --profile-directory=Default

If the answer is wrong, the rules are the problem. If it is right but the click still goes elsewhere, check that Grinch is the default browser and that the rule doesn't depend on the opener or modifier keys, which --test can't reproduce.

2. List the loaded rules

--list-rules prints each rule's index, label, and target as Grinch loaded them:

Terminal
Grinch --list-rules
0: github.com → com.google.Chrome --profile-directory=Default
1: zoom.us/j/* → us.zoom.xos

Rules run top to bottom and the first match wins, so a broad rule above a narrow one hides it. Give rules a name to make the list easier to read.

3. Log real clicks

Set options: { logRequests: true }, reload the config, click the link again, and choose Open Diagnostic Log. Each resolve event records the opener app, the modifier keys, the final URL, the target, and the rule that matched. Match matchedRule.index against --list-rules. Diagnostic log explains every field.

For a full trace on stderr, run the binary with GRINCH_DEBUG=1. console.log calls in your config also print to stderr.

Edits don't take effect

Grinch may be reading a different file. Choose Open Config from the menu to see which one it loaded. It uses the first file that exists in this order:

  1. ~/.grinch.js
  2. ~/.config/grinch.js
  3. ~/.config/grinch/grinch.js
  4. /Library/Application Support/Grinch/grinch.js, a system-wide fallback

Edit the active file, then choose Reload Config. If the reload fails, the menu shows the error and Grinch keeps the last config that loaded. --validate reports the same errors and warnings in a terminal and exits 1 when the config doesn't load:

Terminal
Grinch --validate
grinch: config OK — /Users/alex/.config/grinch.js
rules:  12
Grinch --validate
grinch: config invalid — SyntaxError: Unexpected identifier 'rules'. (line 4)
path:   /Users/alex/.config/grinch.js
echo $?
1

From a terminal, kill -HUP <pid> also reloads the running app's config. Grinch --version (or -V) shows its version.

Warnings don't fail validation. Rule and browser-target config warnings print before the result and change it to config OK with warnings; the exit status stays 0:

Terminal
Grinch --validate
grinch: rules[0].profile is ignored; move it inside the browser target; this rule is suppressed
grinch: config OK with warnings — /Users/alex/.config/grinch.js
rules:  1

Profile warnings also print during validation, but do not change the result to config OK with warnings.

Diagnostic log

Grinch writes one diagnostic log per launch, at:

~/Library/Logs/Grinch/Grinch_<timestamp>.log

It always records config warnings and load errors, JavaScript exceptions at runtime, profile errors, and launch failures. logRequests adds a resolve event for each routed link. The file is created on the first event or when you choose Open Diagnostic Log.

Logs can contain complete URLs, including authentication tokens and personal data. Redact them before sharing. Report suspected vulnerabilities through the security policy.

Formats

The default is JSON, one object per line. Set options: { logFormat: "text" } for readable text. A resolve event in text format:

2026-05-11T12:57:25-0400  resolve
  args: ["--profile-directory=Profile 10"]
  browser: com.google.Chrome
  final: https://example.com/
  matchedRule:
    index: 11
    name: shift-override
  modifiers:
    command: false
    control: false
    option: false
    shift: true
  opener:
    bundleId: com.tinyspeck.slackmacgap
    name: Slack
    pid: 731
  rewritten: false
  strategy: launch_new_instance
  url: https://example.com/

The same event in JSON, which also carries a Unix timestamp:

{
  "date": "2026-05-11T12:57:25-0400",
  "event": "resolve",
  "ts": 1778518645.634,
  "url": "https://example.com/",
  "final": "https://example.com/",
  "rewritten": false,
  "browser": "com.google.Chrome",
  "args": ["--profile-directory=Profile 10"],
  "strategy": "launch_new_instance",
  "opener": {
    "bundleId": "com.tinyspeck.slackmacgap",
    "name": "Slack",
    "pid": 731
  },
  "modifiers": {
    "shift": true,
    "option": false,
    "command": false,
    "control": false
  },
  "matchedRule": {"index": 11, "name": "shift-override"}
}

Fields

date, ts
Local date and time with UTC offset; ts is Unix seconds.
event
resolve, config_warning, config_error, runtime_js_error, profile_error, or launch_error.
rewritten
True when final differs from url. A suppressed URL also becomes about:blank and has rewritten: true.
droppedUrls
On a startup config_error, the number of queued links dropped because the first config load failed.
opener
The app that sent the URL, identified from the Apple Event's sender. An empty bundleId means neither the sender nor the frontmost-app fallback identified one, or the config did not need opener information. Modifier flags are likewise captured only when the config needs them.
matchedRule
{index, name} of the rule that matched, or null when the URL fell through to default. name is the rule's own name, or an automatic label: the string pattern, domain:foo,bar, from:…, running:…, or fn: …. Regex labels also use fn:. Arrays join their labels with | .

A resolve event records the routing decision; it does not confirm the browser opened. launch_error events record invalid URLs, missing browsers, and failures macOS reports after a launch request, with url, browser, strategy, and message; native errors also include errorDomain and errorCode.

Config and JavaScript errors include the config path, when one was found, and the JavaScript message:

{"date":"2026-05-11T12:57:25-0400","event":"config_error","message":"SyntaxError (line 4)","path":"/Users/alex/.grinch.js","ts":1778518645.634}
2026-05-11T12:57:25-0400  config_error
  message: SyntaxError (line 4)
  path: /Users/alex/.grinch.js
Profile errors

profile_error events include the browser bundle ID, the profile-data file path when available, and a message saying what to fix. They report unreadable profile files, profile names Grinch couldn't resolve, and profiles set on browsers that don't support them.

Errors that stop a launch affect the menu bar: an unresolved Chromium profile, an ambiguous or inaccessible Firefox profile, or a profile on a browser that doesn't support profiles. The first such error after a config load changes the icon to ⚠️ and shows Profile error: in the menu, including errors found when a dynamic target runs on a click. Hover over it for the full message. A config error takes precedence in the menu, and a successful reload clears the profile warning unless the error happens again.

Rotation, format changes, and early errors

Rotation (logRotateBytes, logRotateDays) and logFormat take effect once the config's JavaScript has run and its options are parsed, even if Grinch later fails to build the routing engine. Errors before the first config's options are parsed are written as JSON.

Changing logFormat starts a new file and leaves earlier events in the previous one. Files created by format changes use suffixes such as .1.log and .2.log in place of .log. Open Diagnostic Log always opens the current file. Launch errors use the same file and rotation settings, including after config reloads.

console output stays on stderr with its grinch [level]: prefix and is not copied into the log.

Chrome opens a link in a new window

When a rule selects a Chrome profile, Chrome can open a new window even if that profile already has a normal window. This can happen if you most recently used a Chrome web app, popup, or DevTools window for that profile.

To reuse an existing window, activate an ordinary Chrome window for that profile before opening the link. This is tracked in a Chromium issue.

Command-line reference

The binary is /Applications/Grinch.app/Contents/MacOS/Grinch. It accepts:

FlagEffect
--version, -VPrint the version.
--test <url>Run a URL through the rules without opening it. grinch: URLs are unwrapped, so --test grinch:tel:+15551234567 tests routing for tel:+15551234567. Exits 0 when the config loads, 1 if loading fails, or 2 for a missing URL or non-UTF-8 argument.
--list-rulesPrint rule indices, labels, and targets. Exits 0 when the config loads, 1 if loading fails, or 2 for a non-UTF-8 argument.
--list-browsersList every app registered to open https:// URLs: bundle ID and display name.
--validateLoad the config and report whether it parses. Exits 0 on success, including when it prints warnings, and 1 on a load error, printing the message and the path it read. Use it in editor save hooks and CI.
--bench N <url>Resolve a URL N times in-process and report timing.
--bench-init [N]Load the config N times and report timing. Defaults to 1,000 loads.

Except for --version, these commands exit 2 for non-UTF-8 arguments. --bench also exits 2 for missing arguments or a non-positive iteration count; --bench-init exits 2 for an invalid iteration count.