Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ trim_trailing_whitespace = false

[*.sh]
indent_size = 4

; Scripts in ~/.local/bin are extensionless by convention, so [*.sh] misses
; them. Keep them indented like every other shell script in the repo.
[home/dot_local/bin/*]
indent_size = 4
80 changes: 69 additions & 11 deletions docs/wsl.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# 🪟 WSL and 1Password
# 🪟 WSL

A WSL distro is a Linux userland without the pieces a desktop Linux takes for
granted: no local SSH agent holding your keys, and no graphical browser. This
repo wires up both, guarded so nothing leaks into a native Linux or macOS
session.

## SSH keys live in 1Password

In WSL your SSH private keys are not on the Linux side at all — they live in
1Password on the Windows host. Rather than forwarding an *agent socket*, the
1Password on the Windows host. Rather than forwarding an _agent socket_, the
[1Password WSL integration][opwsl] forwards the whole SSH request to the
Windows OpenSSH client (`ssh.exe`), which talks to the 1Password SSH agent and
raises the approval prompt on Windows.

This repo wires that up for you.

## What is configured automatically
### What is configured automatically

| Concern | How | Where |
| --- | --- | --- |
| Git over SSH | `core.sshCommand = ssh.exe` | `dot_config/git/config.tmpl` |
| Interactive `ssh` / `ssh-add` | aliased to `ssh.exe` / `ssh-add.exe` | `dot_config/shell/functions/wsl-ssh.sh`, `dot_config/fish/conf.d/wsl-ssh.fish` |
| Commit signing | `gpg.ssh.program = op-ssh-sign-wsl.exe` | `dot_config/git/config.tmpl` (see [git-signing.md](git-signing.md)) |
| Concern | How | Where |
| ----------------------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
| Git over SSH | `core.sshCommand = ssh.exe` | `dot_config/git/config.tmpl` |
| Interactive `ssh` / `ssh-add` | aliased to `ssh.exe` / `ssh-add.exe` | `dot_config/shell/functions/wsl-ssh.sh`, `dot_config/fish/conf.d/wsl-ssh.fish` |
| Commit signing | `gpg.ssh.program = op-ssh-sign-wsl.exe` | `dot_config/git/config.tmpl` (see [git-signing.md](git-signing.md)) |

The aliases are guarded on `WSL_DISTRO_NAME` **and** on `ssh.exe` being
reachable, so they never leak into a native Linux or macOS session and stay
Expand Down Expand Up @@ -61,7 +66,58 @@ Approving a key authorizes the current WSL session only. A new session or tab
prompts again — the same [authorization model][opauth] 1Password uses
everywhere else.

## What does *not* work in WSL
## Opening links in a browser

A WSL distro has no browser of its own, and no `xdg-open`. Anything that opens
one — `gh auth login`, OAuth flows, `npm docs` — looks for `xdg-open` or
`$BROWSER`, and when neither points anywhere useful it falls back to whatever
terminal browser happens to be installed. Completing an OAuth flow in `lynx` is
not a good time.

`~/.local/bin/wsl-browser` hands the URL to Windows via `powershell.exe
Start-Process`, which opens it in the real default browser. `BROWSER` is
pointed at it, which fixes every tool at once instead of configuring them one
at a time.

| Concern | How | Where |
| ------------- | ------------------------------- | -------------------------------------------------------------------------------------- |
| Opening a URL | `powershell.exe Start-Process` | `dot_local/bin/executable_wsl-browser` |
| `$BROWSER` | exported when running under WSL | `dot_config/shell/functions/wsl-browser.sh`, `dot_config/fish/conf.d/wsl-browser.fish` |

Guarded on `WSL_DISTRO_NAME` **and** on `powershell.exe` being reachable, so it
stays inert on a native Linux session or when [interop][interop] is disabled.

Two deliberate choices in that script:

- **It only accepts `http(s)` URLs or files that exist.** `Start-Process` is
Windows' general "run this" verb, not a browser — handed `calc.exe` or a path
to an executable, it would _run_ it. Since `$BROWSER` is invoked by other
programs, anything else is refused rather than quietly launched.
- **The URL is passed through `WSLENV`, not interpolated** into the PowerShell
command string, so a URL containing quotes or semicolons is always data and
never code.

### Why not wslu?

[wslu][wslu] provides `wslview`, which does the same job and would be the
obvious dependency. It was [archived upstream][wsluarchive] in 2025 — last
release 4.1.3, April 2024 — and it is not packaged for Debian 13. Installing an
unmaintained `.deb` outside `apt` is worse than shipping the few lines above.
If it ever returns to the archives, `wslview` is a drop-in replacement: point
`BROWSER` at it and delete the script.

### Check it

```bash
echo "$BROWSER" # ~/.local/bin/wsl-browser
wsl-browser https://github.com
```

If a tool still opens a terminal browser it is probably remembering its own
setting — `gh`, for example, has `gh config set browser`. Clear it with
`gh config set browser ""` and let `$BROWSER` win.

## What does _not_ work in WSL

1Password **shell plugins** (`op plugin run`) are unsupported under WSL, so the
`gh` / `copilot` wrappers are not applied there. See
Expand All @@ -71,3 +127,5 @@ everywhere else.
[opagent]: https://www.1password.dev/ssh/get-started/
[opauth]: https://www.1password.dev/ssh/agent/security#authorization-model
[interop]: https://learn.microsoft.com/windows/wsl/wsl-config#interop-settings
[wslu]: https://wslutiliti.es/wslu/
[wsluarchive]: https://github.com/wslutilities/wslu
17 changes: 17 additions & 0 deletions home/dot_config/fish/conf.d/wsl-browser.fish
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# WSL -> Windows browser integration for fish.
#
# A WSL distro has no graphical browser and no xdg-open. Tools that "open a
# browser" — `gh auth login`, OAuth flows, `npm docs` — fall back to whatever
# terminal browser is installed (lynx, w3m), which is a miserable way to
# complete an OAuth flow. Pointing BROWSER at the wsl-browser helper in
# ~/.local/bin fixes every such tool at once. See docs/wsl.md.
#
# Guarded on WSL_DISTRO_NAME (set by WSL itself) so this never leaks into a
# native Linux or macOS session, and on powershell.exe being reachable, which
# requires WSL interop to be enabled.

if set -q WSL_DISTRO_NAME; and command -q powershell.exe
if test -x $HOME/.local/bin/wsl-browser
set -gx BROWSER $HOME/.local/bin/wsl-browser
end
end
18 changes: 18 additions & 0 deletions home/dot_config/shell/functions/wsl-browser.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
# WSL -> Windows browser integration for Bash and Zsh.
#
# A WSL distro has no graphical browser and no xdg-open. Tools that "open a
# browser" — `gh auth login`, OAuth flows, `npm docs` — fall back to whatever
# terminal browser is installed (lynx, w3m), which is a miserable way to
# complete an OAuth flow. Pointing BROWSER at the wsl-browser helper in
# ~/.local/bin fixes every such tool at once. See docs/wsl.md.
#
# Guarded on WSL_DISTRO_NAME (set by WSL itself) so this never leaks into a
# native Linux or macOS session, and on powershell.exe being reachable, which
# requires WSL interop to be enabled.

if [ -n "${WSL_DISTRO_NAME:-}" ] && command -v powershell.exe >/dev/null 2>&1 &&
[ -x "${HOME}/.local/bin/wsl-browser" ]; then
BROWSER="${HOME}/.local/bin/wsl-browser"
export BROWSER
fi
44 changes: 44 additions & 0 deletions home/dot_local/bin/executable_wsl-browser
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Open a URL or file in the Windows default browser, from WSL.
#
# WSL has no browser of its own, and no xdg-open. Tools that "open a browser"
# — `gh auth login`, OAuth flows, `npm docs` — fall back to whatever terminal
# browser happens to be installed (lynx, w3m). Completing an OAuth flow in lynx
# is not a good time. Pointing $BROWSER here fixes every such tool at once.
#
# The obvious dependency for this is wslu's `wslview`, but that project was
# archived upstream in 2025 (last release 4.1.3, April 2024) and is not
# packaged for Debian 13. Rather than install an unmaintained .deb outside apt,
# we ship these few lines. See docs/wsl.md.

set -euo pipefail

target=${1:-}
if [ -z "${target}" ]; then
echo "usage: ${0##*/} <url|file>" >&2
exit 2
fi

# Windows' Start-Process is a general "run this" verb, not a browser: handed a
# program name or an .exe path it would execute it. So only pass it things that
# are unambiguously browsable — an http(s) URL, or a file that exists. Anything
# else is refused rather than quietly launched.
if [ -e "${target}" ]; then
target=$(wslpath -w "${target}")
elif [[ ${target} =~ ^https?://[^[:space:]]+$ ]]; then
: # a plain http(s) URL — fine
else
echo "${0##*/}: refusing to open '${target}' (not an http(s) URL or existing file)" >&2
exit 1
fi

# The target is handed over as an environment variable rather than interpolated
# into the PowerShell command string, so a URL containing quotes or semicolons
# is always data and never code. WSLENV is what makes the variable visible to
# the Windows process. `--` stops Start-Process reading a leading `-` as a flag.
export WSLB_TARGET="${target}"
export WSLENV="${WSLENV:+${WSLENV}:}WSLB_TARGET"

# shellcheck disable=SC2016 # $env: is PowerShell syntax, evaluated on the Windows side.
exec powershell.exe -NoProfile -NonInteractive \
-Command 'Start-Process -- $env:WSLB_TARGET'