Skip to main content

Documentation Index

Fetch the complete documentation index at: https://theseventeen-2abbdf80.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

This page walks you through a complete, production-grade Go client that connects to the keychain-auth daemon over Unix domain sockets on macOS and Linux, or Windows Named Pipes on Windows. You will learn how to enforce SOCK_CLOEXEC to prevent file descriptor leaks, send batch write requests, and retrieve secrets in a single round trip using prefix matching.

Full client implementation

The following is a complete, production-grade Go client you can paste into a main.go file and extend for your integration.
package main

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"syscall"
	"time"
)

// Request defines the JSON schema sent by the client.
type Request struct {
	Type       string            `json:"type"`
	Action     string            `json:"action"`
	Service    string            `json:"service"`
	Match      string            `json:"match,omitempty"`
	Targets    []string          `json:"targets,omitempty"`
	Values     []string          `json:"values,omitempty"`
	Attributes map[string]string `json:"attributes,omitempty"`
}

// ResultItem represents a single returned secret from a read or search.
type ResultItem struct {
	Target     string            `json:"target"`
	Value      string            `json:"value,omitempty"`
	Attributes map[string]string `json:"attributes,omitempty"`
}

// Response defines the JSON schema returned by the daemon.
type Response struct {
	Type    string       `json:"type"`
	Status  string       `json:"status"`
	Reason  string       `json:"reason,omitempty"`
	Results []ResultItem `json:"results,omitempty"`
}

// KeychainClient wraps the IPC connection.
type KeychainClient struct {
	conn net.Conn
}

// NewKeychainClient establishes a secure connection to the daemon.
func NewKeychainClient() (*KeychainClient, error) {
	var conn net.Conn
	var err error

	if runtime.GOOS == "windows" {
		// Connect to Windows Named Pipe
		pipePath := `\\.\pipe\keychain-auth`
		conn, err = net.Dial("winio", pipePath) // In production, use "github.com/Microsoft/go-winio"
		if err != nil {
			// Fallback to standard dial if winio is not imported
			conn, err = net.Dial("pipe", pipePath)
		}
	} else {
		// macOS/Linux: use XDG_RUNTIME_DIR on Linux, ~/Library/Application Support on macOS
		socketPath := "/var/run/keychain-auth/agent.sock" // placeholder; override with platform path
		if runtime.GOOS == "darwin" {
			home, _ := os.UserHomeDir()
			socketPath = filepath.Join(home, "Library", "Application Support", "keychain-auth", "agent.sock")
		} else {
			runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
			if runtimeDir == "" {
				home, _ := os.UserHomeDir()
				runtimeDir = filepath.Join(home, ".cache")
			}
			socketPath = filepath.Join(runtimeDir, "keychain-auth", "agent.sock")
		}

		// CRITICAL: We create the socket with SOCK_CLOEXEC to prevent fd leaks across fork/exec
		fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)
		if err != nil {
			return nil, fmt.Errorf("failed to create secure socket: %w", err)
		}

		sa := &syscall.SockaddrUnix{Name: socketPath}
		if err := syscall.Connect(fd, sa); err != nil {
			syscall.Close(fd)
			return nil, fmt.Errorf("failed to connect to daemon socket: %w", err)
		}

		// Convert raw fd to net.Conn
		file := os.NewFile(uintptr(fd), socketPath)
		defer file.Close()
		conn, err = net.FileConn(file)
		if err != nil {
			return nil, fmt.Errorf("failed to convert secure socket to connection: %w", err)
		}
	}

	if err != nil {
		return nil, fmt.Errorf("keychain-auth daemon is not running. Please start it with: keychain-auth start. Error: %w", err)
	}

	return &KeychainClient{conn: conn}, nil
}

func (c *KeychainClient) Close() error {
	return c.conn.Close()
}

// SendRequest sends a request and parses the single-line JSON response.
func (c *KeychainClient) SendRequest(req Request) (*Response, error) {
	c.conn.SetDeadline(time.Now().Add(5 * time.Second))

	// Write Request (must terminate with newline)
	data, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}
	data = append(data, '\n')
	if _, err := c.conn.Write(data); err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}

	// Read single-line response
	reader := bufio.NewReader(c.conn)
	line, err := reader.ReadBytes('\n')
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	var resp Response
	if err := json.Unmarshal(line, &resp); err != nil {
		return nil, fmt.Errorf("failed to parse response JSON: %w", err)
	}

	if resp.Status != "success" {
		return &resp, fmt.Errorf("daemon rejected request: %s", resp.Reason)
	}

	return &resp, nil
}

func main() {
	client, err := NewKeychainClient()
	if err != nil {
		fmt.Printf("Connection Error: %v\n", err)
		os.Exit(1)
	}
	defer client.Close()

	// 1. Write multi-tenant environment keys
	fmt.Println("Saving keys to OS keychain...")
	writeReq := Request{
		Type:    "REQUEST",
		Action:  "write",
		Service: "AgentSecrets",
		Targets: []string{
			"proj_123:development:DATABASE_URL",
			"proj_123:production:DATABASE_URL",
			"proj_123:development:OPENAI_KEY",
		},
		Values: []string{
			"postgres://dev-db",
			"postgres://prod-db",
			"sk-proj-dev123",
		},
	}

	_, err = client.SendRequest(writeReq)
	if err != nil {
		fmt.Printf("Write Failed: %v\n", err)
		return
	}
	fmt.Println("Success!")

	// 2. Perform Single-Roundtrip Prefix Read
	fmt.Println("\nRetrieving keys for development environment using Prefix Read...")
	readReq := Request{
		Type:    "REQUEST",
		Action:  "read",
		Service: "AgentSecrets",
		Match:   "prefix",
		Targets: []string{"proj_123:development:"},
	}

	resp, err := client.SendRequest(readReq)
	if err != nil {
		fmt.Printf("Read Failed: %v\n", err)
		return
	}

	for _, result := range resp.Results {
		fmt.Printf(" -> Key: %-35s Value: %s\n", result.Target, result.Value)
	}
}

Key sections explained

1

Creating the socket with SOCK_CLOEXEC

On macOS and Linux, the client creates the raw file descriptor directly via syscall.Socket rather than using the higher-level net.Dial. The second argument combines syscall.SOCK_STREAM with syscall.SOCK_CLOEXEC in a single atomic syscall:
fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)
This is not a post-creation flag set — the flag is applied atomically at socket creation time, eliminating the race window between socket() and a separate fcntl(FD_CLOEXEC) call.
SOCK_CLOEXEC is mandatory for any process that may fork or exec child processes. Without it, an authenticated socket descriptor is inherited by child processes, allowing untrusted code to issue requests to the daemon as if it were the approved parent binary. Never omit this flag.
2

Cross-platform connection: Unix socket vs Windows Named Pipe

The client detects the platform at runtime using runtime.GOOS and branches accordingly:
if runtime.GOOS == "windows" {
    pipePath := `\\.\pipe\keychain-auth`
    conn, err = net.Dial("winio", pipePath) // use github.com/Microsoft/go-winio
} else if runtime.GOOS == "darwin" {
    home, _ := os.UserHomeDir()
    socketPath = filepath.Join(home, "Library", "Application Support", "keychain-auth", "agent.sock")
    // ... raw syscall path with SOCK_CLOEXEC
} else {
    runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
    if runtimeDir == "" {
        home, _ := os.UserHomeDir()
        runtimeDir = filepath.Join(home, ".cache")
    }
    socketPath = filepath.Join(runtimeDir, "keychain-auth", "agent.sock")
    // ... raw syscall path with SOCK_CLOEXEC
}
On macOS the socket is at ~/Library/Application Support/keychain-auth/agent.sock. On Linux it uses $XDG_RUNTIME_DIR/keychain-auth/agent.sock (fallback: ~/.cache/keychain-auth/agent.sock). On Windows, the Named Pipe path is fixed at \\.\pipe\keychain-auth.
3

Sending a batch write request

A batch write groups multiple targets and values into a single request. The targets and values arrays must have the same length — the daemon rejects mismatched arrays with malformed_request.
writeReq := Request{
    Type:    "REQUEST",
    Action:  "write",
    Service: "AgentSecrets",
    Targets: []string{
        "proj_123:development:DATABASE_URL",
        "proj_123:production:DATABASE_URL",
        "proj_123:development:OPENAI_KEY",
    },
    Values: []string{
        "postgres://dev-db",
        "postgres://prod-db",
        "sk-proj-dev123",
    },
}

_, err = client.SendRequest(writeReq)
The SendRequest method marshals the struct to JSON, appends a newline terminator, and writes it to the connection in one call.
4

Sending a prefix read request

Setting Match to "prefix" and supplying a trailing-slash target tells the daemon to enumerate and return all keys whose names begin with that prefix. This retrieves an entire environment’s secrets in a single round trip instead of N individual reads.
readReq := Request{
    Type:    "REQUEST",
    Action:  "read",
    Service: "AgentSecrets",
    Match:   "prefix",
    Targets: []string{"proj_123:development:"},
}

resp, err := client.SendRequest(readReq)
Your binary policy must have can_search: true for prefix reads to be authorized, because the daemon must enumerate keys server-side to resolve the prefix.
5

Parsing the response

The daemon always responds with a single newline-terminated JSON line. SendRequest reads up to the newline using bufio.Reader.ReadBytes, then unmarshals into the Response struct:
reader := bufio.NewReader(c.conn)
line, err := reader.ReadBytes('\n')
if err != nil {
    return nil, fmt.Errorf("failed to read response: %w", err)
}

var resp Response
if err := json.Unmarshal(line, &resp); err != nil {
    return nil, fmt.Errorf("failed to parse response JSON: %w", err)
}

if resp.Status != "success" {
    return &resp, fmt.Errorf("daemon rejected request: %s", resp.Reason)
}
When Status is not "success", the Reason field contains one of the documented reason codes such as unregistered_binary_pending_approval, service_not_allowed, or action_not_in_policy. Your integration should handle each of these explicitly.