2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00
2026-07-16 12:08:54 +10:00

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 (run zed --version to 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:

  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)

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

<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.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

# 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

S
Description
local editor (using zed but can be anything) callable from remote servers securely.
Readme MIT
49 KiB
Languages
Rust 61.7%
HTML 22.3%
Shell 16%