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 — 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 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 (stable)
- Zed installed and on
$PATH(runzed --versionto confirm) - Linux with systemd (Linux Mint, Ubuntu, Fedora, Arch, etc.)
Build & install
# Clone or extract this project, then:
chmod +x install.sh
./install.sh
The script:
- Runs
cargo build --release - Copies the binary to
~/.local/bin/zed-bridge - Installs the systemd user service
- Creates
~/.config/zed-bridge/config.jsonfrom the example (if not present) - Enables and starts the service
Manual install (if you prefer)
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:
{
"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
<a href="http://localhost:7654/open?repo=mysite&file=content/posts/my-post.md"
target="_blank" rel="noopener">
✏️ Edit in Zed
</a>
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):
<script>
function editInZed(repo, file) {
const url = 'http://localhost:7654/open'
+ '?repo=' + encodeURIComponent(repo)
+ '&file=' + encodeURIComponent(file);
window.open(url, 'zed-bridge',
'width=420,height=160,menubar=no,toolbar=no,location=no,status=no');
}
</script>
Then use buttons anywhere:
<button onclick="editInZed('mysite', 'content/posts/my-post.md')">
✏️ Edit in Zed
</button>
Hugo template
In a list or single template:
{{ if .File }}
<button onclick="editInZed('mysite', '{{ .File.Path }}')">✏️ Edit</button>
{{ end }}
To restrict to dev/local only:
{{ if eq (getenv "HUGO_ENV") "development" }}
<!-- edit button -->
{{ 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:
# 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.1only — not reachable from the network. - All resolved file paths are canonicalised and checked to be inside the declared
repo root before
zedis called (prevents../../etc/passwdstyle traversal). allowed_originoptionally restricts which website can trigger the bridge.- No shell interpolation —
zedis called withCommand::new("zed").arg(path).
Logs & management
# 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:
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