IPC & Headless Mode

May 31, 2026 · View on GitHub

arf includes a built-in IPC (Inter-Process Communication) server that allows external tools to interact with a running R session. This enables AI agents, CI pipelines, and editor extensions to evaluate R code, query session state, and control the session programmatically.

Warning

IPC is an experimental feature. The protocol and CLI interface may change in future versions.

Overview

The IPC system has two parts:

  1. Server — Runs inside arf, listening on a Unix socket (Linux/macOS) or named pipe (Windows)
  2. Client — The arf ipc subcommands that connect to a running server

The server can be enabled in three ways:

MethodUse case
arf headlessCI, AI agents, background R sessions (no terminal needed)
arf --with-ipcInteractive REPL with external tool access
:ipc start meta commandEnable IPC in an already-running session

Headless Mode

Headless mode starts R with an IPC server but without the interactive REPL. This is ideal for environments where no terminal is available.

arf headless

The server runs until interrupted by Ctrl+C, SIGTERM, or arf ipc shutdown. Immediately after startup there is a brief window where the IPC endpoint is listening but R is not yet ready, so early client requests may return R_BUSY. For scripted or automated use, prefer arf headless --json and treat the JSON emitted on stdout as the readiness signal, or retry the first arf ipc command with a small backoff until it succeeds.

Options

OptionDescription
--jsonPrint session info as JSON to stdout when ready (implies --quiet)
--ipc-bind <PATH>Custom socket path (Unix) or named pipe path (Windows)
--ipc-pid-file <PATH>Write PID to file (removed on shutdown)
--log-file <PATH>Redirect log output to file instead of stderr
--history-dir <PATH>Override history database directory
--no-historyDisable command history
--quietSuppress status messages on stderr
--config <PATH>Path to configuration file
--with-r-version <VER>R version to use via rig
--r-home <PATH>Explicit R_HOME path
--vanillaStart R without init files

JSON Output (--json)

When --json is specified, arf prints session connection info to stdout as a single JSON object once the server is ready:

{
  "pid": 12345,
  "socket_path": "/run/user/1000/arf/12345.sock",
  "r_version": "4.4.1",
  "cwd": "/workspace",
  "started_at": "2026-03-22T10:00:00+09:00",
  "log_file": null,
  "warnings": []
}

All keys are always present. r_version and log_file may be null. warnings captures non-fatal startup issues (e.g., config parse errors) that would otherwise only appear on stderr.

Output is pretty-printed when stdout is a terminal, compact when piped. This is useful in CI scripts:

arf headless --json | jq -r .socket_path

R Configuration in Headless Mode

Headless mode automatically configures R for non-interactive use:

  • Pager: Redirected to stdout (no interactive less)
  • Help: Forced to plain text (options(help_type = "text"))
  • Browser: Disabled (URLs are printed instead of opening a browser)
  • Graphics: Defaults to file-based devices (png/pdf) instead of X11
  • Save/Restore: Always --no-save --no-restore-data

IPC Subcommands

All arf ipc subcommands connect to a running arf session. If only one session is active, it is used automatically. When multiple sessions are running, use --pid to target a specific one.

Output Format

All IPC action subcommands (list, eval, send, session, shutdown, history) output JSON to stdout. Output is pretty-printed when stdout is a terminal, compact when piped. Errors are written to stderr as structured JSON:

{
  "error": {
    "code": "R_BUSY",
    "message": "R is busy",
    "hint": "R is executing code. Wait for it to finish, or use 'arf ipc session' to check status.",
    "data": null
  }
}

All four fields (code, message, hint, data) are always present. hint is null when no hint is available. data contains additional structured information (e.g. {"buffer": "..."} for USER_IS_TYPING) or null.

The code field is a string identifier for stable matching. Process exit codes indicate the error category:

Exit codeMeaning
0Success
2IPC transport error (socket/pipe connection failed, connection-level read timeout)
3Session resolution error (no session found, ambiguous PID)
4JSON-RPC protocol error (R busy, user typing, server-side request timeout from --timeout, etc.)

Note: Transport-level timeouts (failing to connect to the socket or low-level read/write timeouts) produce exit code 2 (TRANSPORT_ERROR). Server-side evaluation timeouts triggered by arf ipc eval --timeout result in a JSON-RPC error response from the server, which the client reports as exit code 4.

Error code strings:

CodeExitDescription
TRANSPORT_ERROR2Socket/pipe connection failed, connection-level read timeout, etc.
SESSION_NOT_FOUND3No session with the specified PID, or no sessions at all
SESSION_AMBIGUOUS3Multiple sessions running and --pid not specified
R_BUSY4R is executing code
R_NOT_AT_PROMPT4R is in browser/menu mode
INPUT_ALREADY_PENDING4Another IPC request is already queued
USER_IS_TYPING4User is typing in the REPL (see data fields below)
EMPTY_RESPONSE4Server returned no result
PARSE_ERROR4Invalid JSON in request
INVALID_REQUEST4Not a valid JSON-RPC request
METHOD_NOT_FOUND4Unknown method name
INVALID_PARAMS4Invalid method parameters
INTERNAL_ERROR4Server internal error
PROTOCOL_ERROR4Other JSON-RPC error

USER_IS_TYPING error data

When the user is typing in the REPL, the data field contains:

FieldTypeDescription
bufferstringCurrent editor buffer content (capped at 1024 characters)
buffer_truncatedbooleantrue if the buffer was truncated
buffer_original_lengthintegerFull character count of the original buffer

arf ipc eval — Evaluate R Code

Evaluates R code and returns the captured output. The code runs silently by default — output is not shown in the session.

# Basic evaluation
arf ipc eval '1 + 1'

# With timeout (milliseconds)
arf ipc eval --timeout 10000 'Sys.sleep(5); 42'

# Also show output in the session (REPL or headless stdout)
arf ipc eval --visible 'cat("hello\n")'

# Target a specific session
arf ipc eval --pid 12345 'getwd()'

Parameters:

ParameterDescription
<CODE>R code to evaluate (required)
--visibleAlso show output in the session
--timeout <MS>Timeout in milliseconds (default: 300000 = 5 minutes)
--pid <PID>Target session PID

Output format: JSON object with stdout (string), stderr (string), value (string or null), and error (string or null). All four fields are always present. In silent mode (the default), the printed result appears in the value field rather than stdout. R evaluation errors are included in the error field with exit code 0 — they are a normal response, not an IPC failure.

Example (silent eval, result captured in value):

{
  "stdout": "",
  "stderr": "",
  "value": "[1] 2",
  "error": null
}

Example (R error):

{
  "stdout": "",
  "stderr": "",
  "value": null,
  "error": "object 'x' not found"
}

arf ipc send — Send User Input

Sends code as if the user typed it at the prompt. Output goes to the session's output streams (REPL terminal or headless stdout/log file) and is not captured in the IPC response.

# Send code that appears in the session output
arf ipc send 'library(dplyr)'

# Target a specific session
arf ipc send --pid 12345 'print(mtcars)'

Output format: JSON object with accepted (bool). Example: {"accepted": true}

When to use eval vs send:

evalsend
Output captured in responseYesNo
Shown in sessionOnly with --visibleAlways
User-configurable timeout (--timeout / timeout_ms)YesNo
Use caseProgrammatic accessHuman-visible interaction

arf ipc session — Get Session Info

Returns structured session information as JSON, including arf version, R environment details, and runtime state.

# Pretty-printed on terminal, compact when piped
arf ipc session

# Extract R version with jq
arf ipc session | jq -r '.r.version'

# Check loaded namespaces
arf ipc session | jq '.r.loaded_namespaces'

The response always has the same shape. When R is busy, the r field is null and r_unavailable_reason explains why:

{
  "arf_version": "0.2.6",
  "pid": 12345,
  "os": "linux",
  "arch": "x86_64",
  "socket_path": "/run/user/1000/arf/12345.sock",
  "started_at": "2026-03-22T10:00:00+09:00",
  "log_file": null,
  "r": null,
  "r_unavailable_reason": "R is busy evaluating another expression",
  "hint": null
}

When R is idle, the r field contains session details (other top-level fields omitted for brevity):

{
  "r": {
    "version": "4.4.1",
    "platform": "x86_64-pc-linux-gnu",
    "locale": "en_US.UTF-8",
    "cwd": "/workspace",
    "loaded_namespaces": ["base", "stats", "utils"],
    "attached_packages": ["base", "datasets"],
    "lib_paths": ["/usr/lib/R/library"]
  },
  "r_unavailable_reason": null,
  "hint": null
}

arf ipc list — List Active Sessions

Returns all running arf sessions with IPC enabled as JSON.

arf ipc list

# Example output:
# {
#   "sessions": [
#     {
#       "pid": 12345,
#       "r_version": "4.4.1",
#       "socket_path": "/run/user/1000/arf/12345.sock",
#       "cwd": "/workspace",
#       "started_at": "2026-03-22T10:00:00+09:00",
#       "log_file": null,
#       "history_session_id": 1742601600000000000
#     }
#   ]
# }

When no sessions are running, returns {"sessions": []} (exit 0).

arf ipc history — Query Command History

Returns command history entries from the session's SQLite history database as JSON. By default, only entries from the current session are returned. This method is handled on the server thread and does not touch R, so it works even when R is busy.

# Show recent history from this session (default 50 entries)
arf ipc history

# Show last 10 entries
arf ipc history --limit 10

# Include history from all sessions (not just current)
arf ipc history --all-sessions

# Search for commands containing 'dplyr'
arf ipc history --grep dplyr

# Filter by working directory
arf ipc history --cwd /path/to/project

# Show entries since a date
arf ipc history --since 2026-03-29

# Combine filters
arf ipc history --grep 'library' --limit 20

# Extract commands with jq
arf ipc history | jq -r '.entries[].command'

Parameters:

ParameterDescription
--limit <N>Maximum number of entries to return (default: 50, must be positive)
--all-sessionsInclude entries from all sessions, not just the current one
--cwd <PATH>Filter entries by exact working directory
--grep <PATTERN>Filter entries whose command contains this substring
--since <DATE>Only return entries after this timestamp (RFC 3339 or YYYY-MM-DD)
--pid <PID>Target session PID

Output format: JSON object with entries array (newest first) and session_id. Each entry contains command, timestamp, cwd, exit_status, and session_id (all fields are always present; null when not available). Output is pretty-printed when stdout is a terminal, compact when piped.

Note

Only completed commands are recorded in the history database. A command that is currently executing will not appear in the results until it finishes.

arf ipc shutdown — Shut Down Headless Session

Sends a graceful shutdown request to a headless session. The session cleans up (removes socket, PID file, session file) before exiting.

arf ipc shutdown
arf ipc shutdown --pid 12345

Output format: JSON object with accepted (bool). Example: {"accepted": true}

IPC in Interactive REPL

You can enable IPC in the interactive REPL without headless mode:

Using the --with-ipc Flag

arf --with-ipc

This starts the REPL normally and also starts the IPC server. External tools can then interact with your session while you continue working interactively.

Using Meta Commands

Within a running session, you can start and stop the IPC server:

:ipc start    # Start the IPC server
:ipc stop     # Stop the IPC server
:ipc status   # Show server status

Interactive + IPC: Mutual Exclusion

When both a human and an external tool use the same session, arf prevents conflicts:

  • If you are typing when an IPC eval or send request arrives, the request is rejected with a USER_IS_TYPING error
  • If R is busy (not at the prompt), evaluate requests are rejected immediately with R_BUSY
  • If R is not at the prompt, user_input / send requests are rejected with R_NOT_AT_PROMPT
  • Clients are expected to handle these errors by retrying later (for example, with backoff). In interactive/REPL mode, the server accepts at most one pending request — additional requests are rejected with INPUT_ALREADY_PENDING. In headless mode, requests are queued and processed sequentially
  • The session and history methods do not touch R and can be called even when R is busy or not at the prompt. session always succeeds; history may fail if history is disabled or the history database cannot be accessed
  • list reads local session files and does not connect to any server, so it always works regardless of R state

Transport & Security

Unix (Linux/macOS)

The IPC server listens on a Unix domain socket. The default path depends on the platform:

  • When $XDG_RUNTIME_DIR is set: $XDG_RUNTIME_DIR/arf/<PID>.sock (typically /run/user/<UID>/arf/<PID>.sock on Linux with systemd)
  • When $XDG_RUNTIME_DIR is not set (e.g. macOS): <temp_dir>/arf-<random>/<PID>.sock (where <temp_dir> is the system temp directory, e.g. $TMPDIR; on Linux this is typically /tmp)

The socket directory and file are created with restrictive permissions:

  • Socket directory: mode 0700 (owner only)
  • Socket file (<PID>.sock): mode 0600 (owner only)

Before using the socket directory, arf validates that it is not a symlink, is owned by the current user, and is not writable by group or other users. If validation fails, arf attempts to use a per-process fallback directory instead. If all candidate directories are unsafe or cannot be created, IPC does not start and arf returns an error rather than continuing without IPC.

Session metadata JSON files (<PID>.json) are stored separately in the OS cache directory (e.g., ~/.cache/arf/sessions/ on Linux):

  • Directory: mode 0700 (owner only)
  • File: mode 0600 (owner only)

This prevents other users on the system from discovering or connecting to your session.

Windows

The IPC server listens on a named pipe:

\\.\pipe\arf-ipc-<PID>

Note

Although Windows 10 1803+ supports AF_UNIX sockets, arf currently uses named pipes on Windows because the async runtime (tokio) does not yet support AF_UNIX on Windows. See tokio#2201 for upstream progress.

Custom Bind Path

Use --ipc-bind to specify a custom socket/pipe path. This works for both headless and interactive REPL (--with-ipc) modes:

# Headless — Unix
arf headless --ipc-bind /tmp/my-arf.sock

# Headless — Windows
arf headless --ipc-bind \\.\pipe\my-arf

# Interactive REPL — Unix (editor launches arf and knows the socket path upfront)
arf --with-ipc --ipc-bind /tmp/my-arf.sock

Note

On Unix, the socket file is created with bind() and then restricted to 0600. There is a brief window between these two calls where the default umask applies. If you use a custom path, ensure the parent directory is user-private (e.g., mode 0700) to prevent other users from connecting during that window.

Session Discovery

Each arf session with IPC enabled writes a session file to the OS cache directory (e.g., ~/.cache/arf/sessions/<PID>.json on Linux, ~/Library/Caches/arf/sessions/<PID>.json on macOS). The session file contains the socket path so that arf ipc client commands can discover running sessions. Stale session files (where the process is no longer running) are automatically cleaned up.

Remote Access (No Built-in TCP)

arf intentionally does not listen on TCP. Supporting TCP would require building authentication and encryption into arf itself, which is better handled by dedicated tools. Instead, use an existing proxy or tunnel to expose the local socket remotely:

  • SSH tunneling (OpenSSH 6.7+ supports Unix socket forwarding) — recommended for most cases, as it provides encryption and authentication with no extra software
  • socat — lightweight bidirectional relay between Unix sockets and TCP, useful for quick bridging on trusted networks
  • Reverse proxies (Caddy, nginx) — suitable for persistent setups, especially when TLS is required

On Windows, arf listens on a named pipe which these tools cannot target directly. In WSL environments, npiperelay can bridge a Windows named pipe to stdin/stdout, allowing it to be combined with socat or SSH.

JSON-RPC Protocol

For tool developers who want to communicate with arf directly (without the arf ipc CLI), the server speaks JSON-RPC 2.0 over HTTP on the Unix socket or named pipe.

The server also accepts raw JSON request bodies (without HTTP request-line/headers) for simpler clients. Responses are still sent as standard HTTP/1.1 200 OK messages with headers followed by a JSON body, so raw-JSON clients need to strip the HTTP headers before parsing the response.

Request Format

Send an HTTP POST request with a JSON-RPC body:

POST / HTTP/1.1
Host: localhost
Content-Type: application/json
Content-Length: ...
Connection: close

{"jsonrpc": "2.0", "id": 1, "method": "evaluate", "params": {"code": "1 + 1"}}

Available Methods

MethodParametersDescription
evaluatecode (string), visible (bool, default false), timeout_ms (int, optional)Evaluate R code and return captured output
user_inputcode (string)Send code as user input
session(none)Get session information
historylimit (int, default 50), all_sessions (bool, default false), cwd (string, optional), grep (string, optional), since (string, optional)Query command history
shutdown(none)Shut down the session (headless mode only; returns an error in interactive mode)

Response Examples

Successful evaluation:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "stdout": "",
    "stderr": "",
    "value": "[1] 2"
  }
}

R error:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "stdout": "",
    "stderr": "",
    "error": "object 'x' not found"
  }
}

Errors are caught by tryCatch, so the error message appears in the error field (via conditionMessage()). The stderr field is typically empty for caught errors.

R is busy:

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32000,
    "message": "R is busy"
  }
}

Output Capture

The evaluate method captures R output through two separate channels:

  • stdout / stderr (console output): Captured via R's WriteConsoleEx callback at the C level. This includes text produced by cat(), message(), warning(), and other writes to the R console.
  • value / error (structured result): Captured separately as the evaluated result (or error) of the last expression, using a binary protocol (charToRaw() + writeBin() to a temp file, then read from Rust). In silent evaluate calls, printed values of expressions appear here rather than in stdout/stderr.

The result fields in the JSON response are already properly escaped strings — tool developers do not need to handle the raw binary protocol themselves.

Error Codes

CodeNameDescription
-32700Parse ErrorInvalid JSON
-32600Invalid RequestNot a valid JSON-RPC request
-32601Method Not FoundUnknown method name
-32602Invalid ParamsInvalid method parameters
-32603Internal ErrorServer internal error
-32000R BusyR is executing code
-32001R Not At PromptR has not returned to the prompt
-32002Input Already PendingAnother IPC request is already queued
-32003User Is TypingUser is typing in the REPL (interactive mode only)

Troubleshooting

"No active arf sessions found"

The arf ipc client could not find a session file in the cache directory. This means:

  • No arf session has IPC enabled, or
  • The session file could not be created (for example, the cache directory is missing or not writable), or
  • The session file was cleaned up (the process exited)

Fix: Start arf with arf headless or arf --with-ipc.

"R is busy"

In interactive/REPL mode, the request is rejected immediately with R_BUSY — it is not queued. In headless mode, requests are queued and processed sequentially; clients will typically block until the current operation finishes or their own timeout elapses.

Fix: For interactive mode, handle R_BUSY responses by retrying the request with backoff. In headless mode, configure appropriate client-side timeouts. Note that --timeout only limits how long the IPC call waits for a reply — it does not cancel the underlying R evaluation, and long-running code may keep R busy even after the client times out.

"User is typing" (interactive mode)

In interactive REPL mode, IPC requests are rejected when the user has text in the editor buffer.

Fix: Clear the input line or press Enter before sending the IPC request. This protection prevents IPC from disrupting the user's typing.

Connection refused / timeout

The socket exists but the server is not responding.

Possible causes:

  • The arf process crashed but the socket file was not cleaned up
  • R is stuck in an infinite loop or blocking operation

Fix: Check if the process is still running with arf ipc list. If the session is stale, remove the socket file shown in the socket_path field (for example, $XDG_RUNTIME_DIR/arf/<PID>.sock or <temp_dir>/arf-<random>/<PID>.sock on Unix) and the session metadata file (~/.cache/arf/sessions/<PID>.json).

Permission denied on socket

On Unix, the socket directory and files are created with restrictive permissions (mode 0700/0600). If you see permission errors:

  • Ensure you are connecting as the same user who started arf
  • Check that the socket directory ($XDG_RUNTIME_DIR/arf/ or <temp_dir>/arf-<random>/) has the correct ownership