# How handoffd.bb Serializes tmux Access and Avoids Race Conditions in Swarm-Forge

> Learn how handoffd.bb serializes tmux access and prevents race conditions with single-process ownership, sequential processing, and atomic file moves in Swarm-Forge.

- Repository: [Robert C. Martin/swarm-forge](https://github.com/unclebob/swarm-forge)
- Tags: internals
- Published: 2026-08-30

---

**The `handoffd.bb` daemon prevents tmux race conditions through single-process ownership enforced by a PID file, sequential outbox processing, atomic file moves, and isolated `notify!` wrapper functions that serialize all tmux interactions.**

Swarm-Forge is a Clojure-based tool for managing distributed development sessions, and `handoffd.bb` serves as its dedicated message delivery system. According to the unclebob/swarm-forge source code, this Babashka daemon achieves thread-safe tmux access through a deliberate architectural design that eliminates concurrent operations at multiple levels.

## Single-Process Exclusivity via PID File Locking

The foundation of race condition prevention starts at process initialization. When `handoffd.bb` launches via `(run-daemon!)`, it immediately claims exclusive ownership:

```clojure
;; From handoffd.bb – startup sequence
;; Writes .swarmforge/daemon/handoffd.pid

```

This PID file serves as a system-wide mutex. Subsequent attempts to start the daemon detect the existing process and exit, ensuring **only one instance can ever be alive**. Since `handoffd.bb` is the sole component that communicates with tmux, this guarantees no two processes issue tmux commands concurrently.

The daemon also creates a `stop` flag file and monitors both this file and a `stopping-flag` atom for graceful shutdown. On termination, it removes its PID file, preventing stale locks from blocking restarts.

## Sequential Outbox Processing in poll-once!

Inside `poll-once!`, the daemon loads all configured roles and builds a **deduplicated** list of outbox file paths. It then processes these with a single-threaded `doseq`:

```clojure
;; Main loop – processes each outbox file in strict order
(doseq [path paths
        :while (not (should-stop?))]
  (process-outbox-file! roles socket path))

```

Because the daemon is inherently single-threaded, each handoff file completes full delivery before the next iteration begins. There is no parallelism here—no futures, no agents, no core.async channels that could interleave tmux operations.

## Atomic Delivery Before tmux Notification

The `deliver!` function implements a strict two-phase protocol:

1. **File placement** – copies the handoff file into each recipient's inbox
2. **Notification** – calls `notify!` (the only tmux interaction)
3. **Cleanup** – moves the source file to the sender's *sent* directory

This ordering ensures tmux commands execute only after filesystem operations succeed. The move to `sent-dir` uses `move-with-collision`, which appends a timestamp if the target exists, eliminating filename collisions between concurrent deliveries.

## Isolated notify! Wrapper for tmux Commands

All tmux access flows through the dedicated `notify!` function:

```clojure
(defn notify! [socket session]
  (let [send-text (sh "tmux" "-S" socket "send-keys" "-t" session "-l" wake-message)
        _ (Thread/sleep 150)
        send-cr   (sh "tmux" "-S" socket "send-keys" "-t" session "C-m")
        _ (Thread/sleep 50)
        send-lf   (sh "tmux" "-S" socket "send-keys" "-t" session "C-j")]
    (when-not (zero? (:exit send-text))
      (throw (ex-info "tmux send text failed" send-text)))
    ;; ... additional error handling ...
    ))

```

Key characteristics of this serialization strategy:

- **Socket-specific** – uses the dedicated `.swarmforge/tmux-socket` path
- **Sequential commands** – three `send-keys` invocations with deliberate 150ms and 50ms pauses between them
- **Process-isolated** – no other code path in the codebase calls `sh` with tmux

Because `notify!` always executes within the single-threaded `doseq` loop, these tmux commands can never interleave with another daemon-initiated notification. The sleeps provide tmux session state stability between keystroke sequences.

## Graceful Shutdown and State Consistency

The shutdown protocol protects against race conditions during termination:

1. `stop_handoff_daemon.bb` creates the stop file
2. The daemon detects this via `should-stop?` and sets `stopping-flag`
3. Current `notify!` operations complete (no forced interruption)
4. PID file removal signals clean exit to future startup attempts

This prevents the split-brain scenario where a new daemon starts while the old one still holds the tmux socket.

## Key Source Files

| File | Role |
|------|------|
| `swarmforge/scripts/handoffd.bb` | Core daemon; owns tmux socket, serializes deliveries |
| `swarmforge/scripts/handoff_lib.bb` | Helper functions (`parse-message`, etc.) |
| `swarmforge/scripts/swarmforge.bb` | Launcher that starts `handoffd` per-project |
| `swarmforge/scripts/stop_handoff_daemon.bb` | Creates stop file, triggers graceful shutdown |
| `test/swarmforge/handoff_test.clj` | Validates daemon serialization behavior |

## Summary

- **PID file locking** enforces single-process ownership of tmux access
- **`doseq` iteration** in `poll-once!` guarantees sequential outbox processing
- **Atomic file moves** via `move-with-collision` prevent delivery conflicts
- **Isolated `notify!` function** centralizes and sequences all tmux commands
- **Graceful shutdown** protocol eliminates stale state between daemon restarts

## Frequently Asked Questions

### Why does handoffd.bb use sleeps between tmux commands?

The `Thread/sleep` calls (150ms and 50ms) provide tmux session state stability. According to the source code in `handoffd.bb`, these pauses ensure the `wake-message` text arrives completely before the carriage return (`C-m`) and line feed (`C-j`) execute, preventing partial key sequences during rapid successive notifications.

### What happens if handoffd.bb crashes without removing its PID file?

The startup logic in `run-daemon!` checks for process existence using the stored PID. If the original process no longer exists, the new daemon claims ownership by overwriting the stale PID file. Active process detection prevents false positives from zombie entries.

### Can multiple projects run handoffd.bb simultaneously?

Yes. Each project uses an isolated `.swarmforge` directory with project-specific socket paths and PID files. The serialization guarantees apply per-project; different projects' daemons do not interfere with each other.

### Where is the tmux socket path configured?

The socket path defaults to `.swarmforge/tmux-socket` relative to the project root. This path passes through `poll-once!` and into `notify!` as the `socket` parameter, ensuring all tmux commands target the correct session endpoint.