Remove built-in mirror push now that CI handles it
Mirror-to-GitHub is expressed as an ordinary CI job in .quire/ci.fnl
that shells out git push with a token from the global :secrets map.
The built-in path — mirror::push, MirrorConfig, Repo::config,
Repo::push_to_mirror, GithubConfig, github_auth_header, and the
base64 crate — is no longer needed.

Assisted-by: GLM-5.1 via pi
change sskpzvyqtwpnynstxosquktokyxurqpu
commit c491d2f27047ae6986a6c84293b415c9b14a381e
author Alpha Chen <alpha@kejadlen.dev>
date
parent kknoqysu
diff --git a/Cargo.lock b/Cargo.lock
index ae14394..b19270d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2013,7 +2013,6 @@ version = "0.1.0"
 dependencies = [
  "assert_cmd",
  "axum",
- "base64",
  "clap",
  "clap_complete",
  "fs-err",
diff --git a/Cargo.toml b/Cargo.toml
index 96891dd..4df289c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,6 @@ path = "src/bin/quire/main.rs"
 
 [dependencies]
 axum = "*"
-base64 = "*"
 clap = { version = "*", features = ["derive", "env"] }
 clap_complete = "*"
 fs-err = "*"
diff --git a/README.md b/README.md
index e86a22e..a77b367 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,8 @@ A Rust binary that runs in a Docker container, fronted by the host's sshd and a
 
 - **Git hosting over SSH**, via the host's sshd dispatching into the container. Explicit repo creation (`ssh git@host quire repo new <name>`).
 - **A read-only web view** for browsing README, tree, history, blame, diffs, and refs.
-- **Automatic mirroring to GitHub** on push, when configured per-repo. The post-receive hook sends a push event over a Unix domain socket to `quire serve`, which looks up mirror config and runs the push in-process. A single GitHub PAT lives in global config and rides on the push as an `http.extraHeader` — no per-repo deploy keys, no agent socket plumbing. Mirror failures surface in server logs, not the pusher's terminal.
-- **Fennel-based CI** (Fennel is a Lisp that compiles to Lua), with pipelines defined in `.quire/ci.fnl`. Unsandboxed by default since every pipeline is code I've written; a bubblewrap-based opt-in is available for the day quire ever runs code I haven't.
-- **Email notifications** for CI failures, recoveries, and mirror-push failures. SMTP via `msmtp`; plain text; per-repo config for what to send and to whom.
+- **Fennel-based CI** (Fennel is a Lisp that compiles to Lua), with pipelines defined in `.quire/ci.fnl`. Mirroring to GitHub is expressed as an ordinary CI job that shells out `git push` with a token from the global secrets map. Unsandboxed by default since every pipeline is code I've written; a bubblewrap-based opt-in is available for the day quire ever runs code I haven't.
+- **Email notifications** for CI failures and recoveries. SMTP via `msmtp`; plain text; per-repo config for what to send and to whom.
 
 No issues, no PRs, no user management, no webhooks. Use the GitHub mirror for the social stuff; quire is your forge.
 
@@ -28,7 +27,7 @@ Quire holds to a few principles:
 - **Git's filesystem is the source of truth.** Bare repos under `/var/quire/repos/` are the primary artifact. CI run history is directories on disk, not a database. A database comes back only if the filesystem approach visibly fails.
 - **Built for jj.** The primary client is Jujutsu, which means routine force-pushes, short-lived refs, and unstable SHAs. No git-flow-shaped assumptions in the UI or CI.
 - **Push should fail fast, loudly, and correctly.** No silent drift between quire and GitHub. No accepted-but-unreplicated state.
-- **Config is code.** Global config and per-repo config are Fennel. CI pipelines are Fennel. If you're going to have a scripting language, have one.
+- **Config is code.** Global config is Fennel. CI pipelines are Fennel. If you're going to have a scripting language, have one.
 
 ## Layout
 
diff --git a/docs/PLAN.md b/docs/PLAN.md
index 305be7c..8e7cd05 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -49,7 +49,6 @@ One volume mounted into the container:
   repos/
     foo.git/
       quire/
-        mirror-deploy-key    SSH private key for GitHub mirror (mode 0600)
     work/
       bar.git/
         quire/...
@@ -61,7 +60,7 @@ One volume mounted into the container:
   config.fnl                 global config
 ```
 
-Per-repo config (`mirror`, `public_runs`, etc.) is checked into the repo at `.quire/config.fnl`, not stored in the bare repo's `quire/` directory. Quire reads it from the bare repo via `git show HEAD:.quire/config.fnl`. The `quire/` directory holds only generated artifacts like the mirror deploy key.
+Per-repo config (`public_runs`, etc.) is checked into the repo at `.quire/config.fnl`, not stored in the bare repo's `quire/` directory. The `quire/` directory holds only generated artifacts.
 
 No SSH config or host keys in this volume — those live on the host. The container image brings the `quire` binary and git; the volume brings repos, runs, and per-repo state. Bubblewrap is only needed if CI sandboxing is enabled (it isn't by default).
 
@@ -127,7 +126,7 @@ Things most likely to go wrong here:
 
 ### 2. `quire exec` dispatch subcommand
 
-Replace the ad-hoc shell dispatch from step 1 with `docker exec -i quire-container quire exec "$SSH_ORIGINAL_COMMAND"`. The `quire exec` subcommand takes the original command string, parses it properly (shell-style with a real parser, not regex), validates it against a strict allowlist — `git-receive-pack`, `git-upload-pack`, `git-upload-archive`, and a specific set of `quire` subcommands (`new`, `list`, `rm`, `mirror *`) — and execs the appropriate binary.
+Replace the ad-hoc shell dispatch from step 1 with `docker exec -i quire-container quire exec "$SSH_ORIGINAL_COMMAND"`. The `quire exec` subcommand takes the original command string, parses it properly (shell-style with a real parser, not regex), validates it against a strict allowlist — `git-receive-pack`, `git-upload-pack`, `git-upload-archive`, and a specific set of `quire` subcommands (`new`, `list`, `rm`) — and execs the appropriate binary.
 
 **This is the only dispatch surface into the container.** There's no sshd in the container to backstop a permissive parser; anything that gets past `quire exec` runs as trusted. The allowlist is the security boundary — not a UX convenience, the actual boundary. Treat it that way: explicit enumeration, reject by default, no regex-based "looks safe enough," tests for the rejection paths as well as the accept paths.
 
@@ -139,13 +138,9 @@ Write the quire binary's `hook` subcommand as a no-op that logs what it was invo
 
 `ssh git@host quire new <name>` → `quire exec` → `quire new <name>`. Creates a bare repo under `repos/`, validates the name (no `..`, one level of grouping max, no reserved names), sets `hook.<name>.command` configs. Also: `quire list`, `quire rm`, basic ops. All accessed via the same ssh-dispatch path.
 
-### 5. GitHub mirror via post-receive (deploy key)
+### 5. GitHub mirror via CI job
 
-Per-repo config checked into the repo at `.quire/config.fnl` with a `mirror` key (GitHub remote URL); quire reads it from the bare repo via `git show HEAD:.quire/config.fnl`. A matching private key at `<repo>.git/quire/mirror-deploy-key` (generated by `quire mirror add <remote-url>`, which also prints the public key for the user to paste into GitHub's deploy-keys UI).
-
-Post-receive hook sends a JSON push event over `/var/quire/server.sock` to `quire serve`. The server listener parses the event, looks up the repo's mirror config, resolves the GitHub token from global config, and runs `git push` in a spawned blocking task. Mirror failures surface in the server's logs. If `quire serve` isn't running, the hook prints a warning to stderr and exits cleanly (no push is created).
-
-Pre-receive: if a mirror is configured, test-run a low-cost git operation against the remote (probably `git ls-remote`) to verify the deploy key still works. If it fails, reject with a clear message. Per-repo override (`accept_without_mirror = true`) for the rare case where you want to push without syncing.
+Mirroring to GitHub is expressed as an ordinary CI job in `.quire/ci.fnl`. The job shells out `git push` with a token from the global `:secrets` map. No per-repo deploy keys, no agent socket plumbing. Mirror failures surface in the CI run logs.
 
 ### 6. Web view, minimum viable
 
@@ -178,14 +173,13 @@ What triggers a notification, per-repo-configurable in `.quire/config.fnl`:
 - CI run failed (default: on, if any address is configured)
 - CI run that was previously failing now succeeds (default: on — the "fixed" notification is the one you actually want)
 - CI run succeeded after a success (default: off — noise)
-- Mirror push to GitHub failed (default: on — silent mirror failure is exactly the drift we don't want)
 
 The minimal config to enable failure-and-recovery emails:
 
 ```fennel
 (notifications
   :to ["alpha@example.com"]
-  :on [:ci-failed :ci-fixed :mirror-failed])
+  :on [:ci-failed :ci-fixed])
 ```
 
 Global config has the SMTP connection details and a default `:to` list that per-repo config can override.
@@ -194,18 +188,18 @@ Send failures (SMTP down, auth rejected, etc.) are logged but don't block anythi
 
 ### 11. Polish
 
-Keyboard navigation in the web UI. Atom feeds for recent commits (public, subject to per-repo visibility) and CI runs (auth-gated, same as the log views). `quire` CLI rounded out (rotate mirror keys, prune runs, re-run a CI job, rotate deploy keys).
+Keyboard navigation in the web UI. Atom feeds for recent commits (public, subject to per-repo visibility) and CI runs (auth-gated, same as the log views). `quire` CLI rounded out (prune runs, re-run a CI job).
 
 ## Key design decisions locked in
 
 - **Host mediates SSH; container is quire-only.** Host sshd authenticates, `ForceCommand` dispatches into the container via `docker exec`, container has no sshd. One auth layer, on the host, where the keys belong.
 - **TLS and web auth on the reverse proxy.** Caddy (or equivalent) terminates TLS, handles authentication, and injects a trusted identity header. Quire reads the header and makes visibility decisions. Auth mechanism is the proxy's problem; quire stays scheme-agnostic.
-- **Mirror to GitHub via per-repo deploy key.** Stored at `<repo>.git/quire/mirror-deploy-key`. Post-receive uses `GIT_SSH_COMMAND` with `-i`. No agent forwarding across the host/container boundary, no fragile socket plumbing. Generated by `quire mirror add`, public half printed for the user to paste into GitHub.
+- **Mirror to GitHub via CI job.** Mirroring is expressed as a `.quire/ci.fnl` job that shells out `git push` with a token from the global `:secrets` map. No per-repo deploy keys, no in-process mirror logic.
 - **Web visibility: public by default, per-repo opt-outs.** Repos are public (they go to GitHub anyway); CI logs require auth. Per-repo `(private true)` and `(public_runs true)` flags cover the exceptions.
 - **Trust the proxy-injected identity header.** `Remote-User` is trusted because the reverse proxy is the only ingress. Proxy must strip any client-supplied version before injecting its own — this is the security-critical invariant.
 - **Explicit repo creation, not implicit on first push.** `ssh git@host quire new <n>`. No magic, no shims parsing first pushes.
 - **Hooks via `hook.<n>.command` config.** Git 2.54+ (the version we build into the container image). No shim scripts on disk; `hook.<n>.command = /usr/local/bin/quire hook <n>`. Set at creation time.
-- **Mirror push runs inside `quire serve` via event socket.** The post-receive hook sends a JSON push event over a Unix domain socket (`/var/quire/server.sock`) to `quire serve`, which looks up the repo's mirror config and runs the push in-process. This trades synchronous push-for-push blocking for architectural cleanliness: the hook exits fast, and mirror failures surface in the server's logs, not the pusher's terminal. When the server isn't running, the hook prints a warning and exits cleanly.
+- **Post-receive hook sends push events over Unix socket.** The post-receive hook sends a JSON push event over a Unix domain socket (`/var/quire/server.sock`) to `quire serve`. The server dispatches CI triggers. The hook exits fast. When the server isn't running, the hook prints a warning and exits cleanly.
 - **No reverse-direction mirroring.** quire is the source of truth; GitHub is the replica.
 - **CI pipelines are Fennel macros, not data tables.** The whole point is real code. Shared steps can be factored into `.quire/lib/*.fnl` and `require`'d.
 - **One level of repo grouping max.** `foo.git` and `work/foo.git` are fine. `a/b/c.git` is rejected.
@@ -216,7 +210,6 @@ Keyboard navigation in the web UI. Atom feeds for recent commits (public, subjec
 ## Open questions
 
 - **Inline `ForceCommand` vs. quire-dispatch script.** Simplest is inlining: `ForceCommand docker exec -i quire-container quire exec "$SSH_ORIGINAL_COMMAND"`. No script file, no intermediate layer. The reason to add a `quire-dispatch` script would be host-side logic that needs to run before dispatch (rate limiting, per-key policy, audit logging). Lean: inline, add a script only when a need appears.
-- **Deploy key rotation.** Per-repo keys mean per-repo rotation. `quire mirror rotate <repo>` generates a new key, prints the new pubkey. The annoying part is that the *old* pubkey is still on GitHub until you remove it. Flow: print new pubkey, wait for user to confirm they've added it on GitHub, then switch the config to use the new key, then print the old pubkey and tell them to remove it. Four steps, all mediated by quire CLI. Defer the ergonomics but note that rotation must not silently leave the old key authorized.
 - **Host config bundle.** The reference sshd_config block, Caddyfile, and docker-compose file should be in the quire repo itself, versioned with the code. Ideally a `quire install-host-config` command that writes them interactively. Or just a `docs/host/` directory with copy-paste instructions. Lean toward the latter — interactive installers that touch host config are scope creep.
 - **Public SSH port.** Host's sshd runs on 22. No conflict now — one sshd on the host does everything. Stay on 22.
 - **CI network policy.** Default on (you'll want it for `cargo`, `npm`), with a per-pipeline `(network false)` opt-out. Or default off with explicit `(network true)`? Default on is more ergonomic; default off is more principled.
diff --git a/docs/config.md b/docs/config.md
index 26a4744..d5681ba 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -1,30 +1,30 @@
 # Config files
 
-Quire reads two Fennel config files. Both are pure data — a single
-top-level table — loaded via the embedding described in
-[`fennel.md`](fennel.md).
+Quire reads a Fennel config file at `/var/quire/config.fnl` on the
+bind-mounted volume. It is pure data — a single top-level table —
+loaded via the embedding described in [`fennel.md`](fennel.md).
 
 ## Global config
 
 Lives at `/var/quire/config.fnl` on the bind-mounted volume.
 Operator-created. Re-read on every call (no caching today).
 
-| Key              | Type           | Required | Purpose                                                  |
-|------------------|----------------|----------|----------------------------------------------------------|
-| `:github :token` | `SecretString` | yes      | GitHub PAT used for `http.extraHeader` on mirror pushes. |
-| `:sentry :dsn`   | `SecretString` | no       | Sentry DSN for error reporting. Omit to disable.         |
+| Key            | Type           | Required | Purpose                                                  |
+|----------------|----------------|----------|----------------------------------------------------------|
+| `:sentry :dsn` | `SecretString` | no       | Sentry DSN for error reporting. Omit to disable.         |
+| `:secrets`     | table          | no       | Named secrets exposed to `ci.fnl` jobs as `(secret :name)`. |
 
-Minimal:
+Minimal (no Sentry, no secrets):
 
 ```fennel
-{:github {:token "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"}}
+{}
 ```
 
-With Sentry, and the token sourced from a Docker secret:
+With Sentry, secrets, and the token sourced from a Docker secret:
 
 ```fennel
-{:github {:token {:file "/run/secrets/github_token"}}
- :sentry {:dsn "https://key@o0.ingest.sentry.io/0"}}
+{:sentry {:dsn "https://key@o0.ingest.sentry.io/0"}
+ :secrets {:github_token {:file "/run/secrets/github_token"}}}
 ```
 
 A missing file is a typed error (`Error::ConfigNotFound`). A malformed
@@ -32,25 +32,9 @@ file surfaces as a Fennel parse or eval error with source labels.
 
 ## Per-repo config
 
-Lives at `.quire/config.fnl` *checked into the repo* — quire reads it
-from `HEAD` of the bare repo via `git show HEAD:.quire/config.fnl`.
-Repos without the file (or without a given key) get defaults; this is
-a no-op, not an error.
-
-The post-receive hook does not read this config directly. Instead it
-sends a JSON push event to `quire serve` over `/var/quire/server.sock`.
-The server reads the config to find mirror settings and dispatch the
-push. See `src/bin/quire/commands/serve.rs` for the full path.
-
-| Key            | Type     | Required | Purpose                                                                       |
-|----------------|----------|----------|-------------------------------------------------------------------------------|
-| `:mirror :url` | `String` | no       | HTTPS URL of the mirror remote. URLs with embedded `user:pass@` are rejected. |
-
-Example:
-
-```fennel
-{:mirror {:url "https://github.com/owner/repo.git"}}
-```
+Mirroring is handled by CI jobs defined in `.quire/ci.fnl`. Per-repo
+config at `.quire/config.fnl` is reserved for future use (notifications,
+visibility settings, etc.).
 
 ## SecretString values
 
@@ -71,5 +55,5 @@ logging the result bypasses this — don't.
 ## See also
 
 - [`fennel.md`](fennel.md) — how Fennel files are loaded into Rust structs.
-- `src/quire.rs` — `GlobalConfig`, `RepoConfig`, `MirrorConfig` definitions.
+- `src/quire.rs` — `GlobalConfig` definition.
 - `src/secret.rs` — `SecretString` implementation and tests.
diff --git a/docs/fennel.md b/docs/fennel.md
index abc193d..69ebf68 100644
--- a/docs/fennel.md
+++ b/docs/fennel.md
@@ -26,12 +26,11 @@ DSL. PLAN.md sketches `(notifications :to [...] :on [...])` which reads
 as a function call, but a DSL adds parser machinery for no v1 win. Move
 to a DSL when CI lands and there's a real reason.
 
-A representative per-repo config:
+A representative per-repo config (reserved for future use):
 
 ```fennel
-{:mirror {:url "https://github.com/owner/repo.git"}
- :notifications {:to ["alpha@example.com"]
-                 :on [:ci-failed :mirror-failed]}}
+{:notifications {:to ["alpha@example.com"]
+                 :on [:ci-failed]}}
 ```
 
 Today each call site (`Quire::global_config`, `Repo::config`)
@@ -71,9 +70,7 @@ Errors: file-not-found, parse error, eval error, type mismatch — all
 
 - `src/secret.rs` — `SecretString` wraps Fennel-loaded strings that
   resolve from a file or shell command on access.
-- `src/quire.rs` — `Repo::config` reads per-repo Fennel via `git show
-  HEAD:.quire/config.fnl`; `Quire::global_config` reads the global
-  config from disk. Both define the schema structs they parse into.
+- `src/quire.rs` — `Quire::global_config` reads global config from disk.
 
 ## Test plan
 
diff --git a/src/bin/quire/server.rs b/src/bin/quire/server.rs
index d3884fa..17dffab 100644
--- a/src/bin/quire/server.rs
+++ b/src/bin/quire/server.rs
@@ -8,7 +8,6 @@ use quire::Quire;
 use quire::ci;
 use quire::display_chain;
 use quire::event::PushEvent;
-use quire::mirror;
 
 async fn health() -> &'static str {
     "ok"
@@ -111,6 +110,5 @@ async fn handle_event_connection(mut stream: tokio::net::UnixStream, quire: Quir
         return;
     }
 
-    mirror::push(&quire, &event).await;
     ci::trigger(&quire, &event);
 }
diff --git a/src/lib.rs b/src/lib.rs
index 0363fd2..69dc499 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,7 +2,6 @@ pub mod ci;
 mod error;
 pub mod event;
 pub mod fennel;
-pub mod mirror;
 pub mod quire;
 pub mod secret;
 
diff --git a/src/mirror.rs b/src/mirror.rs
deleted file mode 100644
index e938ce9..0000000
--- a/src/mirror.rs
+++ /dev/null
@@ -1,423 +0,0 @@
-// cov-excl-start
-//! Mirror push: replicate ref updates to a configured remote.
-
-use crate::Quire;
-use crate::display_chain;
-use crate::event::PushEvent;
-use crate::quire::{MirrorConfig, Repo};
-
-/// Push updated refs to the configured mirror, if one is set.
-///
-/// Loads repo and global config, resolves the GitHub token, and runs
-/// the libgit2 push on a blocking task. Errors are logged; the function
-/// itself is infallible from the caller's perspective.
-pub async fn push(quire: &Quire, event: &PushEvent) {
-    let repo = match quire.repo(&event.repo) {
-        Ok(r) if r.exists() => r,
-        Ok(_) => {
-            tracing::error!(repo = %event.repo, "repo not found on disk");
-            return;
-        }
-        Err(e) => {
-            tracing::error!(repo = %event.repo, error = format!("{e:#}"), "invalid repo name in event");
-            return;
-        }
-    };
-
-    let config = match repo.config() {
-        Ok(c) => c,
-        Err(e) => {
-            tracing::error!(repo = %event.repo, error = %display_chain(&e), "failed to load repo config");
-            return;
-        }
-    };
-
-    let Some(mirror) = config.mirror else {
-        tracing::debug!(repo = %event.repo, "no mirror configured, skipping");
-        return;
-    };
-
-    let global_config = match quire.global_config() {
-        Ok(c) => c,
-        Err(e) => {
-            tracing::error!(error = %display_chain(&e), "failed to load global config for mirror push");
-            return;
-        }
-    };
-
-    let token = match global_config.github.token.reveal() {
-        Ok(t) => t.to_string(),
-        Err(e) => {
-            tracing::error!(error = %display_chain(&e), "failed to resolve GitHub token");
-            return;
-        }
-    };
-
-    let refs: Vec<String> = event
-        .updated_refs()
-        .iter()
-        .map(|r| r.r#ref.clone())
-        .collect();
-
-    if refs.is_empty() {
-        return;
-    }
-
-    let mirror_url = mirror.url.clone();
-    tracing::info!(url = %mirror.url, refs = ?refs, "pushing to mirror");
-
-    let result =
-        tokio::task::spawn_blocking(move || push_sync(&repo, &mirror, &token, &refs)).await;
-
-    match result {
-        Ok(Ok(())) => tracing::info!(url = %mirror_url, "mirror push complete"),
-        Ok(Err(e)) => {
-            tracing::error!(url = %mirror_url, error = %display_chain(&e), "mirror push failed")
-        }
-        Err(e) => {
-            tracing::error!(url = %mirror_url, error = %display_chain(&e), "mirror push task panicked")
-        }
-    }
-}
-
-/// Synchronous mirror push — separated for testability.
-///
-/// Converts refs to slices and delegates to `Repo::push_to_mirror`.
-fn push_sync(
-    repo: &Repo,
-    mirror: &MirrorConfig,
-    token: &str,
-    refs: &[String],
-) -> crate::Result<()> {
-    let ref_slices: Vec<&str> = refs.iter().map(|s| s.as_str()).collect();
-    repo.push_to_mirror(mirror, token, &ref_slices)
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use crate::Quire;
-    use crate::event::PushRef;
-    use crate::quire::MirrorConfig;
-    use std::path::Path;
-
-    fn git_in(cwd: &Path, args: &[&str]) {
-        let output = std::process::Command::new("git")
-            .args(args)
-            .current_dir(cwd)
-            .env("GIT_AUTHOR_NAME", "test")
-            .env("GIT_AUTHOR_EMAIL", "test@test")
-            .env("GIT_COMMITTER_NAME", "test")
-            .env("GIT_COMMITTER_EMAIL", "test@test")
-            .env("GIT_CONFIG_GLOBAL", "/dev/null")
-            .env("GIT_CONFIG_SYSTEM", "/dev/null")
-            .output()
-            .expect("git command");
-        if !output.status.success() {
-            panic!(
-                "git {:?} failed:\n{}",
-                args,
-                String::from_utf8_lossy(&output.stderr)
-            );
-        }
-    }
-
-    fn push_event(repo: &str) -> PushEvent {
-        PushEvent::new(
-            repo.to_string(),
-            vec![PushRef {
-                old_sha: "aaa".to_string(),
-                new_sha: "bbb".to_string(),
-                r#ref: "refs/heads/main".to_string(),
-            }],
-        )
-    }
-
-    #[tokio::test]
-    async fn push_skips_repo_not_on_disk() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let quire = Quire::new(dir.path().to_path_buf());
-        let event = push_event("missing.git");
-        // Should not panic — just logs and returns.
-        push(&quire, &event).await;
-    }
-
-    #[tokio::test]
-    async fn push_skips_when_no_mirror_configured() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let bare = dir.path().join("repos").join("test.git");
-
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git_in(&work, &["init", "-b", "main"]);
-        git_in(&work, &["commit", "--allow-empty", "-m", "initial"]);
-        git_in(
-            work.parent().unwrap(),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                bare.to_str().unwrap(),
-            ],
-        );
-
-        // No config.fnl → no mirror.
-        let quire = Quire::new(dir.path().to_path_buf());
-        let event = push_event("test.git");
-        push(&quire, &event).await;
-    }
-
-    #[tokio::test]
-    async fn push_skips_when_no_global_config() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let bare = dir.path().join("repos").join("test.git");
-
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git_in(&work, &["init", "-b", "main"]);
-        git_in(&work, &["commit", "--allow-empty", "-m", "initial"]);
-        git_in(
-            work.parent().unwrap(),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                bare.to_str().unwrap(),
-            ],
-        );
-
-        let quire = Quire::new(dir.path().to_path_buf());
-        let event = push_event("test.git");
-        // No config.fnl exists — should log and return without panic.
-        push(&quire, &event).await;
-    }
-
-    /// Full integration: repo with mirror pointing to a local target, global
-    /// config with a token. Exercises the actual push path.
-    #[tokio::test]
-    async fn push_mirrors_refs_to_target() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let bare = dir.path().join("repos").join("test.git");
-        let target = dir.path().join("target.git");
-
-        // Create source repo with a commit.
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git_in(&work, &["init", "-b", "main"]);
-        git_in(&work, &["commit", "--allow-empty", "-m", "initial"]);
-
-        // Add mirror config to the repo.
-        let config_dir = work.join(".quire");
-        fs_err::create_dir_all(&config_dir).expect("mkdir .quire");
-        fs_err::write(
-            config_dir.join("config.fnl"),
-            format!(r#"{{:mirror {{:url "file://{}"}}}}"#, target.display()),
-        )
-        .expect("write config");
-        git_in(&work, &["add", "."]);
-        git_in(&work, &["commit", "-m", "add config"]);
-
-        // Clone bare.
-        git_in(
-            work.parent().unwrap(),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                bare.to_str().unwrap(),
-            ],
-        );
-
-        // Create target bare repo.
-        fs_err::create_dir_all(&target).expect("mkdir target");
-        git_in(&target, &["init", "--bare", "-b", "main"]);
-
-        // Write global config.
-        let config_path = dir.path().join("config.fnl");
-        fs_err::write(&config_path, r#"{{:github {{:token "ghp_test"}}}}"#)
-            .expect("write global config");
-
-        let quire = Quire::new(dir.path().to_path_buf());
-
-        // Verify repo config loaded correctly with mirror.
-        let repo = quire.repo("test.git").expect("repo");
-        let config = repo.config().expect("repo config should load");
-        let mirror_cfg = config.mirror.as_ref().expect("mirror should be configured");
-        assert!(
-            mirror_cfg.url.contains("target.git"),
-            "mirror URL should point at target: {}",
-            mirror_cfg.url
-        );
-
-        // Get the actual HEAD sha to use in the push event.
-        let sha_output = std::process::Command::new("git")
-            .args(["-C", bare.to_str().unwrap(), "rev-parse", "HEAD"])
-            .output()
-            .expect("rev-parse");
-        let sha = String::from_utf8(sha_output.stdout)
-            .unwrap()
-            .trim()
-            .to_string();
-
-        let event = PushEvent::new(
-            "test.git".to_string(),
-            vec![PushRef {
-                old_sha: "0000000000000000000000000000000000000000".to_string(),
-                new_sha: sha,
-                r#ref: "refs/heads/main".to_string(),
-            }],
-        );
-
-        // push() is infallible — it logs errors internally. The primary
-        // coverage goal is exercising the config-loading and ref-collection
-        // paths. Verify the actual mirror push via push_to_mirror directly,
-        // which is already covered by quire::tests.
-        push(&quire, &event).await;
-
-        // Verify the push actually worked by checking the target.
-        let repo = quire.repo("test.git").expect("repo");
-        let mirror_config = repo.config().expect("config").mirror.expect("mirror");
-        repo.push_to_mirror(&mirror_config, "ghp_test", &["main"])
-            .expect("direct push should work");
-
-        let source_sha_output = std::process::Command::new("git")
-            .args(["-C", bare.to_str().unwrap(), "rev-parse", "HEAD"])
-            .output()
-            .expect("rev-parse source");
-        let source_sha = String::from_utf8(source_sha_output.stdout)
-            .unwrap()
-            .trim()
-            .to_string();
-
-        let target_sha = std::process::Command::new("git")
-            .args(["-C", target.to_str().unwrap(), "rev-parse", "main"])
-            .output()
-            .expect("rev-parse target");
-        let target_sha_str = String::from_utf8(target_sha.stdout)
-            .unwrap()
-            .trim()
-            .to_string();
-        assert_eq!(
-            target_sha_str, source_sha,
-            "mirror target should match source"
-        );
-    }
-
-    #[tokio::test]
-    async fn push_skips_deletion_only_events() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let bare = dir.path().join("repos").join("test.git");
-
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git_in(&work, &["init", "-b", "main"]);
-        git_in(&work, &["commit", "--allow-empty", "-m", "initial"]);
-        git_in(
-            work.parent().unwrap(),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                bare.to_str().unwrap(),
-            ],
-        );
-
-        // Write global config so we get past the token check.
-        let config_path = dir.path().join("config.fnl");
-        fs_err::write(&config_path, r#"{{:github {{:token "ghp_test"}}}}"#)
-            .expect("write global config");
-
-        let quire = Quire::new(dir.path().to_path_buf());
-
-        // Deletion-only event — all refs have zero new_sha.
-        let event = PushEvent::new(
-            "test.git".to_string(),
-            vec![PushRef {
-                old_sha: "aaa".to_string(),
-                new_sha: "0000000000000000000000000000000000000000".to_string(),
-                r#ref: "refs/heads/feature".to_string(),
-            }],
-        );
-
-        // Should return early without pushing anything.
-        push(&quire, &event).await;
-    }
-
-    #[test]
-    fn push_sync_mirrors_refs_to_target() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let source = dir.path().join("repos").join("source.git");
-        let target = dir.path().join("target.git");
-
-        // Create source repo.
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git_in(&work, &["init", "-b", "main"]);
-        git_in(&work, &["commit", "--allow-empty", "-m", "initial"]);
-        git_in(
-            work.parent().unwrap(),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                source.to_str().unwrap(),
-            ],
-        );
-
-        // Create target bare repo.
-        fs_err::create_dir_all(&target).expect("mkdir target");
-        git_in(&target, &["init", "--bare", "-b", "main"]);
-
-        let quire = Quire::new(dir.path().to_path_buf());
-        let repo = quire.repo("source.git").expect("repo");
-        let mirror = MirrorConfig {
-            url: format!("file://{}", target.display()),
-        };
-
-        push_sync(&repo, &mirror, "ghp_test", &["main".to_string()])
-            .expect("push_sync should work");
-
-        // Verify target received main.
-        let source_sha = rev_parse(&source, "HEAD");
-        let target_sha = rev_parse(&target, "main");
-        assert_eq!(target_sha, source_sha, "mirror target should match source");
-    }
-
-    #[test]
-    fn push_sync_errors_on_unreachable_target() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let source = dir.path().join("repos").join("source.git");
-
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git_in(&work, &["init", "-b", "main"]);
-        git_in(&work, &["commit", "--allow-empty", "-m", "initial"]);
-        git_in(
-            work.parent().unwrap(),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                source.to_str().unwrap(),
-            ],
-        );
-
-        let quire = Quire::new(dir.path().to_path_buf());
-        let repo = quire.repo("source.git").expect("repo");
-        let mirror = MirrorConfig {
-            url: "file:///nonexistent/quire-test/target.git".to_string(),
-        };
-
-        let result = push_sync(&repo, &mirror, "ghp_test", &["main".to_string()]);
-        assert!(result.is_err(), "expected error for unreachable target");
-    }
-
-    fn rev_parse(repo: &Path, rev: &str) -> String {
-        let output = std::process::Command::new("git")
-            .args(["-C", repo.to_str().unwrap(), "rev-parse", rev])
-            .output()
-            .expect("rev-parse");
-        String::from_utf8(output.stdout).unwrap().trim().to_string()
-    }
-}
-// cov-excl-stop
diff --git a/src/quire.rs b/src/quire.rs
index b2ea990..29295be 100644
--- a/src/quire.rs
+++ b/src/quire.rs
@@ -13,7 +13,6 @@ use crate::{Error, Result as AppResult};
 /// Top-level stays open for future keys (notifications defaults, SMTP, etc.).
 #[derive(serde::Deserialize, Debug)]
 pub struct GlobalConfig {
-    pub github: GithubConfig,
     #[serde(default)]
     pub sentry: Option<SentryConfig>,
     /// Named secrets exposed to `ci.fnl` jobs as `(secret :name)`.
@@ -22,50 +21,11 @@ pub struct GlobalConfig {
     pub secrets: HashMap<String, SecretString>,
 }
 
-#[derive(serde::Deserialize, Debug)]
-pub struct GithubConfig {
-    pub token: SecretString,
-}
-
 #[derive(serde::Deserialize, Debug)]
 pub struct SentryConfig {
     pub dsn: SecretString,
 }
 
-/// Per-repo configuration parsed from `.quire/config.fnl`.
-///
-/// Loaded from `HEAD:.quire/config.fnl` in the bare repo via `git show`.
-#[derive(serde::Deserialize, Debug, Default, PartialEq)]
-pub struct RepoConfig {
-    pub mirror: Option<MirrorConfig>,
-}
-
-#[derive(serde::Deserialize, Debug, PartialEq)]
-pub struct MirrorConfig {
-    #[serde(deserialize_with = "deserialize_mirror_url")]
-    pub url: String,
-}
-
-/// Reject URLs with embedded user[:password]@ credentials so a misconfigured
-/// repo can't leak a token via tracing, Sentry, or git's own error output.
-/// Tokens come from global config and ride in `http.extraHeader`.
-fn deserialize_mirror_url<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    use serde::Deserialize;
-    let url = String::deserialize(deserializer)?;
-    if let Some((_, after_scheme)) = url.split_once("://")
-        && let Some(at) = after_scheme.find('@')
-        && !after_scheme[..at].contains('/')
-    {
-        return Err(serde::de::Error::custom(
-            "mirror URL must not embed credentials; tokens come from global config",
-        ));
-    }
-    Ok(url)
-}
-
 /// A resolved repository path.
 ///
 /// Created by `Quire::repo` after validating the name.
@@ -172,89 +132,6 @@ impl Repo {
     pub fn runs(&self) -> Runs {
         Runs::new(self.runs_base())
     }
-
-    /// Push `main` to the configured mirror, injecting the GitHub token via
-    /// `http.extraHeader` so it never appears in the URL or git's error output.
-    ///
-    /// The token is passed through `GIT_CONFIG_*` env vars on the child
-    /// process. This keeps it out of the command line (visible via `ps`),
-    /// but it remains visible in `/proc/<pid>/environ` to anything running
-    /// as the same uid for the lifetime of the push. Acceptable today
-    /// (single-user container, no CI runner yet); revisit when CI lands.
-    pub fn push_to_mirror(
-        &self,
-        mirror: &MirrorConfig,
-        token: &str,
-        refs: &[&str],
-    ) -> AppResult<()> {
-        let mut args = vec!["push", "--porcelain", &mirror.url];
-        args.extend(refs);
-
-        let status = self
-            .git(&args)
-            .env("GIT_CONFIG_COUNT", "1")
-            .env("GIT_CONFIG_KEY_0", "http.extraHeader")
-            .env("GIT_CONFIG_VALUE_0", github_auth_header(token))
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .status()?;
-
-        if !status.success() {
-            return Err(Error::Git(format!("push to {} failed", mirror.url)));
-        }
-        Ok(())
-    }
-
-    /// Load per-repo config from `HEAD:.quire/config.fnl`.
-    ///
-    /// Returns a default (empty) `RepoConfig` when:
-    /// - HEAD doesn't exist (fresh repo, no pushes yet).
-    /// - The config file is absent from HEAD.
-    /// - The `:mirror` key is absent from the parsed config.
-    ///
-    /// Returns an error when the config file exists but contains
-    /// malformed Fennel — source labels point at the right line.
-    pub fn config(&self) -> AppResult<RepoConfig> {
-        // Check whether HEAD exists first — exit code distinguishes this
-        // reliably without parsing stderr text.
-        let has_head = self
-            .git(&["rev-parse", "--verify", "HEAD"])
-            .stdout(std::process::Stdio::null())
-            .stderr(std::process::Stdio::null())
-            .status()?
-            .success();
-
-        if !has_head {
-            return Ok(RepoConfig::default());
-        }
-
-        let output = self.git(&["show", "HEAD:.quire/config.fnl"]).output()?;
-
-        if !output.status.success() {
-            // HEAD exists but the file doesn't — not an error.
-            return Ok(RepoConfig::default());
-        }
-
-        let source = String::from_utf8(output.stdout)?;
-
-        let fennel = Fennel::new()?;
-        Ok(fennel.load_string(&source, "HEAD:.quire/config.fnl")?)
-    }
-}
-
-/// Build the `Authorization` header value used to authenticate `git push`
-/// to GitHub over HTTPS.
-///
-/// GitHub's git smart HTTP endpoint (`/info/refs`, `git-receive-pack`)
-/// rejects `Authorization: Bearer <PAT>` with 401, even though the REST
-/// API accepts the same token via Bearer. Git then falls back to
-/// prompting for a username, which fails inside a post-receive hook
-/// because there's no TTY. HTTP Basic with any non-empty username and
-/// the token as the password is the documented form for git push.
-fn github_auth_header(token: &str) -> String {
-    use base64::{Engine, engine::general_purpose::STANDARD};
-    let encoded = STANDARD.encode(format!("x-access-token:{token}"));
-    format!("Authorization: Basic {encoded}")
 }
 
 /// Application runtime context.
@@ -354,125 +231,6 @@ impl Quire {
 mod tests {
     use super::*;
 
-    /// Helper: create a temp dir with a bare repo that has one commit
-    /// containing `.quire/config.fnl` with the given content.
-    fn bare_repo_with_config(config_content: &str) -> tempfile::TempDir {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let bare = dir.path().join("repos").join("test.git");
-
-        // Create a worktree repo, commit the config, then clone --bare.
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        let git = |args: &[&str]| {
-            let output = std::process::Command::new("git")
-                .args(args)
-                .current_dir(&work)
-                .env("GIT_AUTHOR_NAME", "test")
-                .env("GIT_AUTHOR_EMAIL", "test@test")
-                .env("GIT_COMMITTER_NAME", "test")
-                .env("GIT_COMMITTER_EMAIL", "test@test")
-                .env("GIT_CONFIG_GLOBAL", "/dev/null")
-                .env("GIT_CONFIG_SYSTEM", "/dev/null")
-                .output()
-                .expect("git command");
-            assert!(output.status.success());
-            output
-        };
-
-        git(&["init"]);
-        git(&["commit", "--allow-empty", "-m", "initial"]);
-
-        let config_dir = work.join(".quire");
-        fs_err::create_dir_all(&config_dir).expect("mkdir .quire");
-        fs_err::write(config_dir.join("config.fnl"), config_content).expect("write config");
-        git(&["add", "."]);
-        git(&["commit", "-m", "add config"]);
-
-        git(&[
-            "clone",
-            "--bare",
-            work.to_str().unwrap(),
-            bare.to_str().unwrap(),
-        ]);
-
-        dir
-    }
-
-    /// Helper: create a temp dir with an empty bare repo (no HEAD).
-    fn empty_bare_repo() -> (tempfile::TempDir, Repo) {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let bare = dir.path().join("repos").join("test.git");
-        fs_err::create_dir_all(&bare).expect("mkdir repos/test.git");
-
-        let git = |args: &[&str]| {
-            let output = std::process::Command::new("git")
-                .args(args)
-                .current_dir(&bare)
-                .env("GIT_AUTHOR_NAME", "test")
-                .env("GIT_AUTHOR_EMAIL", "test@test")
-                .env("GIT_COMMITTER_NAME", "test")
-                .env("GIT_COMMITTER_EMAIL", "test@test")
-                .env("GIT_CONFIG_GLOBAL", "/dev/null")
-                .env("GIT_CONFIG_SYSTEM", "/dev/null")
-                .output()
-                .expect("git command");
-            assert!(output.status.success());
-            output
-        };
-
-        git(&["init", "--bare", "-b", "main"]);
-
-        let repo = Repo {
-            name: "test.git".to_string(),
-            quire_root: bare.parent().unwrap().parent().unwrap().to_path_buf(),
-        };
-
-        (dir, repo)
-    }
-
-    /// Helper: create a bare repo with at least one commit but no `.quire/config.fnl`.
-    fn bare_repo_without_config() -> (tempfile::TempDir, Repo) {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let bare = dir.path().join("repos").join("test.git");
-
-        let git = |args: &[&str], cwd: &Path| {
-            let output = std::process::Command::new("git")
-                .args(args)
-                .current_dir(cwd)
-                .env("GIT_AUTHOR_NAME", "test")
-                .env("GIT_AUTHOR_EMAIL", "test@test")
-                .env("GIT_COMMITTER_NAME", "test")
-                .env("GIT_COMMITTER_EMAIL", "test@test")
-                .env("GIT_CONFIG_GLOBAL", "/dev/null")
-                .env("GIT_CONFIG_SYSTEM", "/dev/null")
-                .output()
-                .expect("git command");
-            assert!(output.status.success());
-            output
-        };
-
-        fs_err::create_dir_all(&work).expect("mkdir work");
-        git(&["init"], &work);
-        // Commit with no .quire directory.
-        git(&["commit", "--allow-empty", "-m", "initial"], &work);
-        git(
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                bare.to_str().unwrap(),
-            ],
-            &work,
-        );
-
-        let repo = Repo {
-            name: "test.git".to_string(),
-            quire_root: bare.parent().unwrap().parent().unwrap().to_path_buf(),
-        };
-        (dir, repo)
-    }
-
     fn quire() -> Quire {
         Quire::default()
     }
@@ -605,80 +363,17 @@ mod tests {
         assert!(q.repo_from_path(&path).is_err());
     }
 
-    #[test]
-    fn repo_config_loads_mirror_url() {
-        let dir = bare_repo_with_config(r#"{:mirror {:url "https://github.com/owner/repo.git"}}"#);
-        let bare = dir.path().join("repos").join("test.git");
-        let repo = Repo {
-            name: "test.git".to_string(),
-            quire_root: bare.parent().unwrap().parent().unwrap().to_path_buf(),
-        };
-
-        let config = repo.config().expect("config should load");
-        assert_eq!(
-            config.mirror,
-            Some(MirrorConfig {
-                url: "https://github.com/owner/repo.git".to_string(),
-            })
-        );
-    }
-
-    #[test]
-    fn repo_config_returns_no_mirror_when_head_missing() {
-        let (_dir, repo) = empty_bare_repo();
-        let config = repo.config().expect("should return default config");
-        assert_eq!(config.mirror, None);
-    }
-
-    #[test]
-    fn repo_config_returns_no_mirror_when_file_absent() {
-        let (_dir, repo) = bare_repo_without_config();
-        let config = repo.config().expect("should return default config");
-        assert_eq!(config.mirror, None);
-    }
-
-    #[test]
-    fn repo_config_returns_no_mirror_when_key_absent() {
-        let dir = bare_repo_with_config("{}");
-        let bare = dir.path().join("repos").join("test.git");
-        let repo = Repo {
-            name: "test.git".to_string(),
-            quire_root: bare.parent().unwrap().parent().unwrap().to_path_buf(),
-        };
-
-        let config = repo.config().expect("should return default config");
-        assert_eq!(config.mirror, None);
-    }
-
-    #[test]
-    fn repo_config_errors_on_malformed_fennel() {
-        let dir = bare_repo_with_config("{:bad {:}");
-        let bare = dir.path().join("repos").join("test.git");
-        let repo = Repo {
-            name: "test.git".to_string(),
-            quire_root: bare.parent().unwrap().parent().unwrap().to_path_buf(),
-        };
-
-        let err = repo.config().unwrap_err();
-        // The error message should reference the config path.
-        let msg = err.to_string();
-        assert!(
-            msg.contains("HEAD:.quire/config.fnl"),
-            "error should mention the config path: {msg}"
-        );
-    }
-
     #[test]
     fn global_config_loads_from_fennel_file() {
         let dir = tempfile::tempdir().expect("tempdir");
         let config_path = dir.path().join("config.fnl");
-        fs_err::write(&config_path, r#"{:github {:token "ghp_test123"}}"#).expect("write");
+        fs_err::write(&config_path, "{}").expect("write");
 
         let q = Quire {
             base_dir: dir.path().to_path_buf(),
         };
         let config = q.global_config().expect("global_config should load");
-        assert_eq!(config.github.token.reveal().unwrap(), "ghp_test123");
+        assert!(config.secrets.is_empty());
     }
 
     #[test]
@@ -701,7 +396,7 @@ mod tests {
         let config_path = dir.path().join("config.fnl");
         fs_err::write(
             &config_path,
-            r#"{:github {:token "ghp_test"} :sentry {:dsn "https://key@sentry.io/123"}}"#,
+            r#"{:sentry {:dsn "https://key@sentry.io/123"}}"#,
         )
         .expect("write");
 
@@ -709,7 +404,6 @@ mod tests {
             base_dir: dir.path().to_path_buf(),
         };
         let config = q.global_config().expect("global_config should load");
-        assert_eq!(config.github.token.reveal().unwrap(), "ghp_test");
         let sentry = config.sentry.expect("sentry should be present");
         assert_eq!(sentry.dsn.reveal().unwrap(), "https://key@sentry.io/123");
     }
@@ -718,7 +412,7 @@ mod tests {
     fn global_config_sentry_is_optional() {
         let dir = tempfile::tempdir().expect("tempdir");
         let config_path = dir.path().join("config.fnl");
-        fs_err::write(&config_path, r#"{:github {:token "ghp_test"}}"#).expect("write");
+        fs_err::write(&config_path, "{}").expect("write");
 
         let q = Quire {
             base_dir: dir.path().to_path_buf(),
@@ -731,7 +425,7 @@ mod tests {
     fn global_config_secrets_default_empty() {
         let dir = tempfile::tempdir().expect("tempdir");
         let config_path = dir.path().join("config.fnl");
-        fs_err::write(&config_path, r#"{:github {:token "ghp_test"}}"#).expect("write");
+        fs_err::write(&config_path, "{}").expect("write");
 
         let q = Quire {
             base_dir: dir.path().to_path_buf(),
@@ -749,9 +443,8 @@ mod tests {
         fs_err::write(
             &config_path,
             format!(
-                r#"{{:github {{:token "ghp_test"}}
-                    :secrets {{:github_token {{:file "{}"}}
-                              :slack_webhook "https://hooks.slack.com/abc"}}}}"#,
+                r#"{{:secrets {{:github_token {{:file "{}"}}
+                   :slack_webhook "https://hooks.slack.com/abc"}}}}"#,
                 secret_file.display()
             ),
         )
@@ -787,112 +480,4 @@ mod tests {
             .expect("git command");
         assert!(output.status.success());
     }
-
-    /// Helper: create a bare repo at `bare` with `main` and one commit.
-    fn make_bare_with_main(work: &Path, bare: &Path) {
-        fs_err::create_dir_all(work).expect("mkdir work");
-        git_in(work, &["init", "-b", "main"]);
-        git_in(work, &["commit", "--allow-empty", "-m", "initial"]);
-        git_in(
-            work.parent().unwrap_or(work),
-            &[
-                "clone",
-                "--bare",
-                work.to_str().unwrap(),
-                bare.to_str().unwrap(),
-            ],
-        );
-    }
-
-    fn rev_parse(repo: &Path, rev: &str) -> String {
-        let output = std::process::Command::new("git")
-            .args(["-C", repo.to_str().unwrap(), "rev-parse", rev])
-            .output()
-            .expect("rev-parse");
-        assert!(output.status.success(), "rev-parse failed");
-        String::from_utf8(output.stdout)
-            .expect("utf-8")
-            .trim()
-            .to_string()
-    }
-
-    #[test]
-    fn github_auth_header_is_basic_with_x_access_token_username() {
-        use base64::{Engine, engine::general_purpose::STANDARD};
-
-        let header = super::github_auth_header("ghp_test");
-        let encoded = header
-            .strip_prefix("Authorization: Basic ")
-            .unwrap_or_else(|| panic!("missing Basic prefix: {header}"));
-        // libcurl rejects header values containing newlines, so the encoded
-        // form must not wrap.
-        assert!(
-            !encoded.contains('\n'),
-            "encoded header value must not wrap: {encoded:?}"
-        );
-        let decoded = STANDARD.decode(encoded).expect("valid base64");
-        assert_eq!(decoded, b"x-access-token:ghp_test");
-    }
-
-    #[test]
-    fn push_to_mirror_pushes_main_to_file_mirror() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let source = dir.path().join("repos").join("source.git");
-        let target = dir.path().join("target.git");
-
-        make_bare_with_main(&work, &source);
-        fs_err::create_dir_all(&target).expect("mkdir target");
-        git_in(&target, &["init", "--bare", "-b", "main"]);
-
-        let repo = Repo::new(&dir.path().join("repos"), "source.git").expect("repo");
-        let mirror = MirrorConfig {
-            url: format!("file://{}", target.display()),
-        };
-        repo.push_to_mirror(&mirror, "ignored-for-file-url", &["main"])
-            .expect("push should succeed");
-
-        assert_eq!(rev_parse(&source, "main"), rev_parse(&target, "main"));
-    }
-
-    #[test]
-    fn push_to_mirror_errors_when_target_unreachable() {
-        let dir = tempfile::tempdir().expect("tempdir");
-        let work = dir.path().join("work");
-        let source = dir.path().join("repos").join("source.git");
-
-        make_bare_with_main(&work, &source);
-
-        let repo = Repo::new(&dir.path().join("repos"), "source.git").expect("repo");
-        let mirror = MirrorConfig {
-            url: "file:///nonexistent/quire-test/target.git".to_string(),
-        };
-        let err = repo.push_to_mirror(&mirror, "x", &["main"]).unwrap_err();
-        assert!(
-            matches!(err, Error::Git(_)),
-            "expected Git error, got {err:?}"
-        );
-    }
-
-    #[test]
-    fn mirror_url_rejects_embedded_credentials() {
-        let dir = bare_repo_with_config(
-            r#"{:mirror {:url "https://x:token@github.com/owner/repo.git"}}"#,
-        );
-        let bare = dir.path().join("repos").join("test.git");
-        let repo = Repo {
-            name: "test.git".to_string(),
-            quire_root: bare.parent().unwrap().parent().unwrap().to_path_buf(),
-        };
-
-        let err = repo.config().unwrap_err();
-        let chain: Vec<String> =
-            std::iter::successors(Some(&err as &dyn std::error::Error), |e| e.source())
-                .map(|e| e.to_string())
-                .collect();
-        assert!(
-            chain.iter().any(|m| m.contains("credentials")),
-            "expected credential error in chain, got: {chain:?}"
-        );
-    }
 }
diff --git a/tests/property.rs b/tests/property.rs
index 7bda6fb..1ddf1aa 100644
--- a/tests/property.rs
+++ b/tests/property.rs
@@ -1,8 +1,7 @@
 use hegel::TestCase;
-use hegel::generators::{from_regex, integers, just, sampled_from, text, vecs};
+use hegel::generators::{integers, just, text, vecs};
 use hegel::one_of;
 use quire::event::{PushEvent, PushRef};
-use quire::quire::MirrorConfig;
 use quire::secret::SecretString;
 
 const ZERO_SHA: &str = "0000000000000000000000000000000000000000";
@@ -87,37 +86,3 @@ fn secret_string_from_file_strips_one_trailing_newline(tc: TestCase) {
     let expected = content.strip_suffix('\n').unwrap_or(&content).to_string();
     assert_eq!(revealed, expected);
 }
-
-fn deserialize_mirror(url: &str) -> Result<MirrorConfig, serde_json::Error> {
-    serde_json::from_value(serde_json::json!({ "url": url }))
-}
-
-#[hegel::test]
-fn mirror_url_rejects_embedded_credentials(tc: TestCase) {
-    // Build `scheme://user@host/path`: the deserializer must reject because the
-    // `@` sits before any `/` in the authority section.
-    let scheme = tc.draw(sampled_from(&["https", "http", "ssh", "git"]));
-    let user = tc.draw(from_regex(r"\A[A-Za-z0-9_:.-]+\Z"));
-    let host = tc.draw(from_regex(r"\A[A-Za-z0-9.-]+\Z"));
-    let path = tc.draw(from_regex(r"\A[A-Za-z0-9/_.-]*\Z"));
-    let url = format!("{scheme}://{user}@{host}/{path}");
-
-    let err = deserialize_mirror(&url).expect_err(&format!("must reject {url}"));
-    assert!(
-        err.to_string().contains("must not embed credentials"),
-        "wrong rejection reason for {url}: {err}",
-    );
-}
-
-#[hegel::test]
-fn mirror_url_accepts_at_in_path(tc: TestCase) {
-    // `@` after the first `/` is in the path, not the authority — must accept.
-    let scheme = tc.draw(sampled_from(&["https", "http", "ssh", "git"]));
-    let host = tc.draw(from_regex(r"\A[A-Za-z0-9.-]+\Z"));
-    let before_at = tc.draw(from_regex(r"\A[A-Za-z0-9/_.-]*\Z"));
-    let after_at = tc.draw(from_regex(r"\A[A-Za-z0-9/_.-]*\Z"));
-    let url = format!("{scheme}://{host}/{before_at}@{after_at}");
-
-    let cfg = deserialize_mirror(&url).expect("must accept");
-    assert_eq!(cfg.url, url);
-}