# zed-bridge
A tiny Rust HTTP server that runs on your local workstation at `http://localhost:7654`.
Click an **Edit in Zed** button on any remote website you control, and the file opens
instantly in the [Zed editor](https://zed.dev) — pointing directly at your
already-checked-out copy.
Zero external Rust dependencies (only `serde` / `serde_json` for config).
Binds to `127.0.0.1` only — nothing reachable from outside your machine.
---
## Why?
You run a private or internal site (a [Hugo](https://gohugo.io) blog, a docs site, a
CMS, a wiki) hosted on a remote server. The content lives in a git repo you also have
checked out locally. When you spot a typo or want to expand a page while browsing the
live site, you want to be editing it in Zed **immediately** — not `cd`-ing through
directories, guessing paths, or opening your editor manually.
zed-bridge lets you drop an **Edit in Zed** button on any page of the remote site.
One click and the file is open in your local Zed editor, at the exact path you were
just looking at. No SSH, no sync, no finding the file — just edit and go.
It's designed for **sites you control** (private dev blogs, internal docs, Hugo
preview environments). It is not a generic remote-editing service: the bridge only
opens files in repos you've explicitly allowlisted in its config, and it only listens
on `127.0.0.1`.
---
## How it works
```
Your browser (remote HTTPS site)
│
│ window.open("http://localhost:7654/open?repo=mysite&file=content/posts/foo.md")
│
▼
zed-bridge (systemd user service, always running)
│
├─ validates repo name against ~/.config/zed-bridge/config.json
├─ resolves & canonicalises file path (blocks path traversal)
├─ optional: checks git staleness (local cached state, no network call)
└─ launches: zed /home/you/repos/mysite/content/posts/foo.md
```
The pop-up tab shows a result page (success or error) then closes itself after 1.8 s.
---
## Installation
### Prerequisites
- [Rust](https://rustup.rs) (stable)
- [Zed](https://zed.dev) installed and on `$PATH` (run `zed --version` to confirm)
- Linux with systemd (Linux Mint, Ubuntu, Fedora, Arch, etc.)
### Build & install
```bash
# Clone or extract this project, then:
chmod +x install.sh
./install.sh
```
The script:
1. Runs `cargo build --release`
2. Copies the binary to `~/.local/bin/zed-bridge`
3. Installs the systemd user service
4. Creates `~/.config/zed-bridge/config.json` from the example (if not present)
5. Enables and starts the service
### Manual install (if you prefer)
```bash
cargo build --release
cp target/release/zed-bridge ~/.local/bin/
cp zed-bridge.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now zed-bridge
```
---
## Configuration
Edit `~/.config/zed-bridge/config.json`:
```json
{
"port": 7654,
"check_git": true,
"allowed_origin": null,
"repos": {
"mysite": "~/repos/mysite",
"blog": "~/repos/blog",
"docs": "~/repos/docs"
}
}
```
| Field | Default | Description |
|---|---|---|
| `port` | `7654` | Local port to listen on |
| `check_git` | `true` | Show warning if local branch is behind upstream |
| `allowed_origin` | `null` | If set, only requests from this origin are accepted (e.g. `"https://mysite.example.com"`) |
| `repos` | `{}` | Map of short name → local path. Supports `~` expansion. |
The service reads config on every request, so changes take effect immediately — no restart needed.
---
## Adding Edit buttons to your remote site
### Quickest — plain link
```html
✏️ Edit in Zed
```
This opens a small browser tab that closes itself after 1.8 s.
### Better UX — small pop-up
Add this JS once (e.g. in your base layout footer):
```html
```
Then use buttons anywhere:
```html
```
### Hugo template
In a list or single template:
```html
{{ if .File }}
{{ end }}
```
To restrict to dev/local only:
```html
{{ if eq (getenv "HUGO_ENV") "development" }}
{{ end }}
```
Or check the request IP in your nginx config and set a header, then gate on it in your template.
### Nginx — show edit buttons only from your own IP
In your nginx server block:
```nginx
# Only pass the header when the request comes from your workstation/VPN
geo $show_edit_button {
default 0;
203.0.113.42 1; # your static IP
100.64.0.0/10 1; # Tailscale range
}
server {
...
location / {
proxy_set_header X-Dev-Edit $show_edit_button;
...
}
}
```
Then in your app/template, check `X-Dev-Edit: 1` to conditionally render the buttons.
---
## API reference
### `GET /open`
Opens a file in Zed.
| Parameter | Required | Description |
|---|---|---|
| `repo` | ✅ | Repo key from config (e.g. `mysite`) |
| `file` | ✅ | Relative path within the repo (e.g. `content/posts/foo.md`) |
**Responses:** All return HTML. `200` on success (Zed launched). `400` bad params.
`403` forbidden (path traversal or origin check failed). `404` repo/file not found.
`500` config error or Zed failed to launch.
### `GET /health`
Returns `{"status":"ok","service":"zed-bridge"}` — useful for checking the service
is alive from the browser (see the status indicator in `example/edit-button.html`).
---
## Git staleness check
When `check_git` is `true`, the bridge runs:
```
git rev-list --left-right --count HEAD...@{upstream}
```
This reads from the **locally cached tracking ref** — it is instant and makes no
network calls. The result reflects the state at your last `git fetch` or `git pull`.
If your local branch is behind, a yellow warning appears in the result pop-up before
Zed opens. The file is still opened — it's a warning, not a block.
To disable: set `"check_git": false` in config.
---
## Security
- Binds to `127.0.0.1` only — not reachable from the network.
- All resolved file paths are canonicalised and checked to be inside the declared
repo root before `zed` is called (prevents `../../etc/passwd` style traversal).
- `allowed_origin` optionally restricts which website can trigger the bridge.
- No shell interpolation — `zed` is called with `Command::new("zed").arg(path)`.
---
## Logs & management
```bash
# View live logs
journalctl --user -u zed-bridge -f
# Restart after editing config or upgrading
systemctl --user restart zed-bridge
# Stop
systemctl --user stop zed-bridge
# Disable autostart
systemctl --user disable zed-bridge
```
---
## Troubleshooting
**"Zed not found"** — Make sure `zed` is on your `$PATH`. Check with `which zed`.
The systemd user service inherits your login session's PATH; if Zed is installed
in a non-standard location, add `Environment=PATH=...` to the service file.
**Button does nothing / CORS error** — Browsers block `fetch()` to localhost from
HTTPS pages (Private Network Access), but `window.open()` still works. Use the
pop-up approach shown above, not `fetch()`.
**Pop-up blocked** — Some browsers block `window.open` unless triggered directly by
a user click. Make sure the `editInZed()` call is inside a click handler, not a timeout.
**Service won't start on login** — Ensure systemd user lingering is enabled if you
need it to start without an interactive session:
```bash
loginctl enable-linger $USER
```
---
## File structure
```
zed-bridge/
├── src/
│ └── main.rs # Full server — no async, no HTTP framework
├── example/
│ └── edit-button.html # Live demo + copy-paste snippets
├── Cargo.toml
├── config.example.json
├── zed-bridge.service # systemd user unit
├── install.sh
├── test.sh
├── LICENSE
└── README.md
```
---
## License
MIT