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 covers a complete Node.js client that connects to the keychain-auth daemon over Unix domain sockets on macOS and Linux, and Windows Named Pipes on Windows. You will learn how to detect the correct socket path at runtime, use net.createConnection for both transports, parse newline-delimited responses from a streaming buffer, and structure batch write and prefix read requests using async/await.

Full client implementation

The following is a complete Node.js client you can drop into a .js file and require or import the KeychainAuthClient class from your integration code.
const net = require('net');
const os = require('os');
const path = require('path');

class KeychainAuthClient {
    constructor() {
        if (process.platform === 'win32') {
            this.socketPath = '\\\\.\\pipe\\keychain-auth';
        } else if (process.platform === 'darwin') {
            this.socketPath = path.join(os.homedir(), 'Library', 'Application Support', 'keychain-auth', 'agent.sock');
        } else {
            const runtimeDir = process.env.XDG_RUNTIME_DIR || path.join(os.homedir(), '.cache');
            this.socketPath = path.join(runtimeDir, 'keychain-auth', 'agent.sock');
        }
        
        this.client = null;
    }

    connect() {
        return new Promise((resolve, reject) => {
            this.client = net.createConnection(this.socketPath, () => {
                // Connection established
                resolve();
            });

            this.client.on('error', (err) => {
                reject(new Error(`keychain-auth daemon is not running. Start with: keychain-auth start. Details: ${err.message}`));
            });
        });
    }

    sendRequest(requestObj) {
        return new Promise((resolve, reject) => {
            let buffer = '';
            
            // Set up single-line JSON parsing
            const onData = (data) => {
                buffer += data.toString();
                if (buffer.includes('\n')) {
                    const line = buffer.split('\n')[0];
                    this.client.removeListener('data', onData);
                    try {
                        const response = JSON.parse(line);
                        if (response.status !== 'success') {
                            reject(new Error(`Daemon rejected request: ${response.reason}`));
                        } else {
                            resolve(response);
                        }
                    } catch (e) {
                        reject(new Error(`Failed to parse response JSON: ${e.message}`));
                    }
                }
            };

            this.client.on('data', onData);
            
            // Send payload (must terminate with newline)
            this.client.write(JSON.stringify(requestObj) + '\n');
        });
    }

    close() {
        if (this.client) {
            this.client.end();
        }
    }
}

// ==========================================
// Runtime Demo
// ==========================================
(async () => {
    try {
        const client = new KeychainAuthClient();
        await client.connect();
        
        // 1. Batch Write Keys
        console.log("Writing secrets...");
        const writeReq = {
            type: "REQUEST",
            action: "write",
            service: "AgentSecrets",
            targets: ["proj_123:development:DATABASE_URL", "proj_123:development:OPENAI_KEY"],
            values: ["mongodb://localhost/dev", "sk-proj-node123"]
        };
        await client.sendRequest(writeReq);
        console.log("Success!");

        // 2. Prefix Read
        console.log("\nReading development config via Prefix Read...");
        const readReq = {
            type: "REQUEST",
            action: "read",
            service: "AgentSecrets",
            match: "prefix",
            targets: ["proj_123:development:"]
        };
        const response = await client.sendRequest(readReq);
        
        response.results.forEach(res => {
            console.log(` -> Key: ${res.target} = ${res.value}`);
        });
        
        client.close();
    } catch (e) {
        console.error("Execution Error:", e.message);
    }
})();

Key sections explained

1

Platform detection for socket path

The constructor sets this.socketPath based on process.platform before any connection is made:
if (process.platform === 'win32') {
    this.socketPath = '\\\\.\\pipe\\keychain-auth';
} else if (process.platform === 'darwin') {
    this.socketPath = path.join(os.homedir(), 'Library', 'Application Support', 'keychain-auth', 'agent.sock');
} else {
    const runtimeDir = process.env.XDG_RUNTIME_DIR || path.join(os.homedir(), '.cache');
    this.socketPath = path.join(runtimeDir, 'keychain-auth', 'agent.sock');
}
On Windows the path is the Named Pipe \\.\pipe\keychain-auth. On macOS the socket is in ~/Library/Application Support/keychain-auth/. On Linux it uses $XDG_RUNTIME_DIR/keychain-auth/ (fallback: ~/.cache/keychain-auth/).
2

net.createConnection usage

net.createConnection accepts both Unix socket paths and Windows Named Pipe paths transparently. On Unix it creates a SOCK_STREAM socket; on Windows it opens the Named Pipe. No platform-specific branching is needed at the connection call site.
connect() {
    return new Promise((resolve, reject) => {
        this.client = net.createConnection(this.socketPath, () => {
            resolve();
        });

        this.client.on('error', (err) => {
            reject(new Error(`keychain-auth daemon is not running. Start with: keychain-auth start. Details: ${err.message}`));
        });
    });
}
The connect callback fires once the connection is fully established. The 'error' event fires if the socket path does not exist or the daemon is not accepting connections.
When the daemon is not running, net.createConnection emits an 'error' event with err.code === 'ENOENT' (socket file missing) or 'ECONNREFUSED' (socket exists but daemon not listening). You can check err.code in the error handler to distinguish “daemon not started” from other I/O errors and surface an actionable message such as keychain-auth start.
3

Newline-delimited response parsing with buffer

Node.js streams deliver data in arbitrary chunks. The client accumulates incoming bytes in a string buffer and only processes a response once a \n is detected:
let buffer = '';

const onData = (data) => {
    buffer += data.toString();
    if (buffer.includes('\n')) {
        const line = buffer.split('\n')[0];
        this.client.removeListener('data', onData);
        // parse line...
    }
};

this.client.on('data', onData);
After finding the newline the listener is removed immediately with removeListener. This is important for a long-lived connection where multiple sequential requests are made: without removing the handler, a second onData listener registered for the next request would also fire on data intended for a prior request’s handler.
4

Promise-based request/response pattern

sendRequest wraps the event-driven socket API in a Promise, giving callers a clean async/await interface:
sendRequest(requestObj) {
    return new Promise((resolve, reject) => {
        // attach onData handler, then write
        this.client.write(JSON.stringify(requestObj) + '\n');
    });
}
The handler is registered before the write to eliminate any race where a very fast daemon response arrives before the listener is attached. The Promise resolves with the parsed response object on success, or rejects with an Error whose message includes the daemon’s reason code on failure.
5

Batch write and prefix read examples

A batch write sends multiple targets and values in one request. The targets and values arrays must be the same length:
const writeReq = {
    type: "REQUEST",
    action: "write",
    service: "AgentSecrets",
    targets: ["proj_123:development:DATABASE_URL", "proj_123:development:OPENAI_KEY"],
    values: ["mongodb://localhost/dev", "sk-proj-node123"]
};
await client.sendRequest(writeReq);
A prefix read uses "match": "prefix" and a trailing-slash target to retrieve all secrets under a namespace in a single round trip. The daemon returns an array of { target, value } objects for every key that matches the prefix:
const readReq = {
    type: "REQUEST",
    action: "read",
    service: "AgentSecrets",
    match: "prefix",
    targets: ["proj_123:development:"]
};
const response = await client.sendRequest(readReq);

response.results.forEach(res => {
    console.log(` -> Key: ${res.target} = ${res.value}`);
});
Your binary’s policy must have can_search: true for prefix reads to be authorized, since the daemon enumerates keys server-side to resolve the prefix.