IPC Communication Method Between CLI and Daemon Socket in no-mistakes
The no-mistakes CLI communicates with its background daemon through a lightweight RPC protocol over Unix domain sockets (or Windows named pipes), using length-prefixed JSON messages for request-response cycles.
The no-mistakes project implements a custom inter-process communication (IPC) layer that enables the command-line interface to control a persistent background daemon. This IPC communication method abstracts transport details so the same RPC protocol operates seamlessly across Linux, macOS, and Windows environments.
How the IPC Architecture Works
The communication stack follows a client-server model where the CLI acts as the client and the daemon hosts the server. The implementation resides in the internal/ipc package, split across platform-specific transport layers and shared protocol logic.
Socket Path Resolution
When the CLI initializes, it invokes ipc.NewClient() to establish connectivity. The client queries the transport layer for the socket address via ipc.SocketPath(), which resolves to platform-specific locations:
- Unix systems:
filepath.Join(os.TempDir(), "no-mistakes.sock")or$NM_HOME/daemon.sock - Windows: Named pipe at
\\.\pipe\no-mistakes
Connection Establishment
The client dials the resolved address using platform-appropriate system calls. In internal/ipc/client.go, the connection logic selects the transport implementation based on runtime.GOOS:
func (c *Client) dial() (net.Conn, error) {
if runtime.GOOS == "windows" {
return windowsDial(ipc.SocketPath())
}
return net.Dial("unix", ipc.SocketPath())
}
On Unix, this uses standard net.Dial with the "unix" network type, while Windows utilizes named pipe APIs via winproc.DialPipe.
Message Framing Protocol
All RPC messages serialize to JSON and prepend a 4-byte little-endian integer indicating payload length. This framing mechanism, defined in internal/ipc/protocol.go, ensures the receiver can delineate complete messages from stream data even when the underlying socket delivers partial buffers.
The wire format consists of:
- 4 bytes: Payload length (uint32, little-endian)
- N bytes: JSON-encoded
RequestorResponsestruct containingmethod,params, andresultfields
Request-Response Cycle
The IPC communication method follows a synchronous call pattern:
- The CLI invokes
client.Call(method, params, &result) - The client marshals a
protocol.Requeststruct, applies length-prefix framing, and writes to the socket - The daemon's server routine (in
internal/ipc/server.go) accepts the connection, reads the frame, and unmarshals the request - The server dispatches to the registered handler for the specified
method - The handler returns a
protocol.Responsecontaining either a result payload or an error - The client unmarshals the response into the provided result pointer
Cross-Platform Transport Abstraction
The repository isolates platform-specific socket implementations to maximize code reuse:
internal/ipc/transport_unix.go: Implementsnet.Conninterface over Unix domain sockets located in the daemon's home directory, restricting access to the current userinternal/ipc/transport_windows.go: Provides identical semantics using Windows named pipes with the same logical naming convention
This abstraction allows the RPC layer in client.go and server.go to remain platform-agnostic while leveraging native IPC mechanisms optimized for each operating system.
Implementation Examples
CLI Side: Sending a Status Request
The following code from the CLI demonstrates initializing the IPC client and invoking the status method:
package main
import (
"log"
"github.com/kunchenguid/no-mistakes/internal/ipc"
)
func main() {
// Create a client that knows how to talk to the daemon.
c, err := ipc.NewClient()
if err != nil {
log.Fatalf("cannot create IPC client: %v", err)
}
defer c.Close()
// Perform an RPC call; the daemon implements the "status" method.
var status ipc.StatusResponse
if err := c.Call("status", nil, &status); err != nil {
log.Fatalf("daemon error: %v", err)
}
log.Printf("daemon version: %s, running: %t", status.Version, status.Running)
}
Daemon Side: Registering the Status Handler
The daemon registers method handlers using server.Handle():
package daemon
import (
"github.com/kunchenguid/no-mistakes/internal/ipc"
)
func registerHandlers(s *ipc.Server) {
s.Handle("status", func(_ *ipc.Request) (*ipc.Response, error) {
// Gather whatever information the CLI cares about.
resp := ipc.StatusResponse{
Version: "v1.2.3",
Running: true,
}
return ipc.NewResponse(resp), nil
})
}
Key Source Files
The IPC communication method relies on these components:
internal/ipc/client.go: ImplementsNewClient(),Call(), and connection management for the CLI sideinternal/ipc/server.go: Handles socket listening, connection acceptance, and request dispatching in the daemoninternal/ipc/protocol.go: DefinesRequestandResponsestructs along with length-prefix framing logicinternal/ipc/transport_unix.go: Unix-domain socket implementation detailsinternal/ipc/transport_windows.go: Windows named pipe implementation detailsinternal/daemon/manager.go: Boots the IPC server when the daemon initializes
Summary
- The IPC communication method uses a custom RPC protocol over local sockets, avoiding heavy external dependencies
- Length-prefixed JSON framing ensures reliable message boundaries across stream-based transports
- Platform abstraction via
transport_unix.goandtransport_windows.goenables single-binary cross-platform support - Synchronous request-response pattern keeps CLI logic straightforward while the daemon handles asynchronous workloads
- Security derives from filesystem permissions on Unix sockets (located in
$NM_HOME) and named pipe ACLs on Windows
Frequently Asked Questions
What transport protocols does no-mistakes use for IPC?
On Linux and macOS, the system uses Unix domain sockets created in either the system temporary directory or the daemon's home directory. On Windows, it uses named pipes with the identifier \\.\pipe\no-mistakes. Both transports implement the standard net.Conn interface, allowing the RPC layer to remain identical across platforms.
How does the daemon handle multiple concurrent CLI requests?
The server implementation in internal/ipc/server.go accepts connections in a loop, spawning goroutines to handle each client connection independently. This allows multiple CLI instances to communicate with the daemon simultaneously without blocking the main server thread.
Is the IPC communication secure?
Yes, by leveraging filesystem-level security. Unix domain sockets are created within the daemon's private $NM_HOME directory with restricted permissions, ensuring only the owning user can connect. Windows named pipes inherit the default security descriptor, which restricts access to the same user session running the daemon.
What happens if the daemon socket is deleted while running?
If the socket file is deleted externally while the daemon operates, existing connections remain active because they operate on file descriptors already established. However, new CLI instances cannot connect until the daemon restarts and recreates the socket. The daemon's Serve() loop in server.go monitors for net.ErrClosed to detect shutdown conditions gracefully.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →