This document is the authoritative specification for the amigactl wire protocol. Daemon and client implementations MUST conform to the behavior described here. Where protocol-commands.md specifies per-command semantics, this document specifies framing, encoding, connection lifecycle, error codes, and binary transfer conventions that apply to all commands.
All text on the wire is ISO-8859-1 (Latin-1). This is the native character encoding of AmigaOS. Clients MUST encode outgoing text as ISO-8859-1 and decode incoming text as ISO-8859-1.
Bytes 0x00 through 0x1F (except 0x0A LF and 0x0D CR) have no defined meaning in the protocol and MUST NOT appear in command arguments or text payload data. Binary DATA chunk bodies are exempt from this restriction (see Binary Data Framing). Their presence in a text line is not an error -- the daemon processes the line as-is -- but behavior is undefined.
The protocol runs over a single TCP connection. The default listening port is 6800. The daemon accepts up to 8 simultaneous clients.
Connections beyond the limit are closed immediately by the server with no banner and no error message.
The client opens a TCP connection to the daemon's listening port.
The daemon checks the client's IP address against its access control list
(configured via ALLOW directives in S:amigactld.conf). If the ACL is
non-empty and the client's IP is not listed, the daemon closes the
connection immediately. No banner is sent. No error message is sent.
If the ACL is empty (no ALLOW directives), all IPs are permitted.
On successful connection, the daemon sends a banner line:
AMIGACTL <version>\n
<version> is a dotted version string. The client
SHOULD read and validate the banner before sending any commands. The
banner is not followed by a sentinel -- it is a single line, not a
response envelope.
The client sends commands and reads responses, one at a time. See the Request Format and Response Format sections below.
Either side may close the connection at any time:
-
Client-initiated (graceful): The client sends
QUIT\n. The daemon responds withOK Goodbye\n.\nand then closes the connection. -
Client-initiated (abrupt): The client closes the TCP connection without sending QUIT. The daemon detects EOF on the next read, cleans up client state, and frees the slot.
-
Server-initiated: The daemon closes the connection after processing a QUIT command, after a SHUTDOWN sequence, or on Ctrl-C shutdown. The client detects EOF on the next read.
The canonical line ending is LF (\n, 0x0A).
For telnet compatibility, the daemon also accepts CR LF (\r\n,
0x0D 0x0A). When the daemon encounters a CR immediately preceding a LF
in a request line, it strips the CR before processing. CR characters in
other positions are not stripped and are treated as part of the line
content.
The daemon MUST send responses using bare LF line endings. Clients SHOULD accept bare LF. Clients MAY also tolerate CR LF in responses for robustness, but the daemon will never send CR LF.
Request lines that are empty or contain only whitespace (spaces and tabs) before the line ending are silently ignored. The daemon sends no response for such lines. This allows clients to send blank lines as keepalives or for readability without triggering errors.
A request is a single line of text:
COMMAND [arguments]\n
-
COMMAND is a verb (e.g.,
VERSION,PING,DIR). Commands are case-insensitive:ping,Ping, andPINGare equivalent. -
Arguments follow the command, separated by a space. Argument syntax is command-specific and defined in protocol-commands.md.
-
The maximum request line length is 4096 bytes, including the command, arguments, and any trailing CR, but excluding the terminating LF.
If the daemon's receive buffer fills (4096 bytes) without encountering a LF, the request is oversized. The daemon:
- Sends
ERR 100 Command too long\n.\n. - Enters discard mode for this client: all subsequent incoming data is discarded until a LF is found.
- Once a LF is found, the daemon exits discard mode. The connection is now ready for the next command.
The connection is NOT closed. The client can recover by ensuring its oversized data is terminated with LF, then resuming normal commands.
Every command produces exactly one response. Every response is terminated by a sentinel line (see below). There are two response types:
OK [info]\n
[payload line 1]\n
[payload line 2]\n
...
.\n
-
The status line begins with
OK. It may optionally include additional information after a space (e.g.,OK Goodbye,OK rc=0,OK 14832). If there is no additional information, the line is justOK\n. -
Zero or more payload lines follow. Payload content is command-specific and defined in protocol-commands.md. Payload lines are subject to dot-stuffing (see below).
-
The response is terminated by the sentinel: a line consisting of a single period followed by LF (
.\n).
ERR <code> <message>\n
.\n
-
The status line begins with
ERR, followed by a space, a numeric error code, a space, and a human-readable error message. -
Error responses have no payload lines. The sentinel immediately follows the status line.
Every response -- both OK and ERR -- is terminated by a sentinel line
(.\n). No exceptions.
This invariant simplifies client implementations: a client reads lines
until it encounters a line that is exactly .\n (after dot-unstuffing).
At that point, the response is complete.
The sentinel is always the last line of a response. Nothing follows it until the client sends the next command.
Exception: Streaming responses. The TAIL and TRACE commands produce
ongoing streaming responses where DATA chunks may arrive at any time
after the OK status line, for an indefinite duration. The sentinel is
sent only when the stream terminates (via client STOP, process exit for
TRACE RUN, or server error). During a TAIL or TRACE stream, the client
MAY send STOP to request termination. See the ARexx and Streaming
Wire Formats section for details.
Payload lines may contain arbitrary text, including lines that begin with a period. To prevent ambiguity with the sentinel, the protocol uses dot-stuffing (the same mechanism used by SMTP):
-
Sending (daemon): If a payload line begins with
.(0x2E), the daemon prepends an additional.before sending. A payload line.foois sent as..foo\n. A payload line consisting of a single.is sent as..\n. -
Receiving (client): When the client reads a line that begins with
., it checks whether the line is exactly.\n(the sentinel). If the line begins with.., the client removes the leading.to recover the original payload line.
The sentinel .\n is never dot-stuffed. It is always exactly two bytes:
0x2E 0x0A.
Dot-stuffing applies only to payload lines in OK responses. The
status line (OK ... or ERR ...) and the banner are never dot-stuffed.
Some commands (READ, WRITE, APPEND, EXEC, AREXX, TAIL, TRACE) transfer binary or large data that cannot be reliably represented as dot-stuffed text lines. These commands use DATA/END chunked framing within the response envelope.
OK <filesize>\n
DATA <chunk_len>\n
<raw bytes: exactly chunk_len bytes>
DATA <chunk_len>\n
<raw bytes: exactly chunk_len bytes>
...
END\n
.\n
-
The OK status line includes the total file size in bytes as its info field.
-
Each
DATAline specifies the number of raw bytes that immediately follow. The chunk length is a decimal integer. The maximum chunk size is 4096 bytes. -
After the
DATAline's LF, exactlychunk_lenraw bytes follow. These bytes are not line-oriented and are not dot-stuffed. They may contain any byte value including 0x0A, 0x0D, and 0x2E. -
The receiver MUST read exactly
chunk_lenbytes by looping onrecv()before expecting the nextDATAorENDline. TCP does not guarantee delivery boundaries. -
After the last chunk, the daemon sends
END\nfollowed by the sentinel.\n. -
A zero-length file produces:
OK 0\nEND\n.\n(no DATA chunks).
Client: WRITE <path> <total_size>\n
Server: READY\n
Client: DATA <chunk_len>\n<raw bytes>
Client: DATA <chunk_len>\n<raw bytes>
...
Client: END\n
Server: OK <bytes_written>\n.\n
-
The client sends the WRITE command with the target path and total file size.
-
The server validates the path and responds with
READY\nto indicate it is prepared to receive data.READYis not an OK/ERR response -- it is a handshake signal and is not followed by a sentinel. -
The client then sends DATA chunks in the same format as READ but in the client-to-server direction. The same chunk size limit (4096 bytes) applies.
-
After the last chunk, the client sends
END\n. -
The server writes the data to a temporary file (
~act.tmp) in the same directory as the target path, then atomically renames it to the target. On success, the server responds withOK <bytes_written>\n.\n. -
Error during transfer: If the server encounters a write error during the data transfer phase (disk full, I/O error), it closes the file, deletes the temporary file, and disconnects the client (TCP close). No ERR response is sent. The client detects this as EOF on the next read. This matches the behavior for malformed DATA lines and client disconnect -- any unrecoverable error during the transfer phase results in connection teardown.
-
Partial WRITE on disconnect: If the client disconnects before sending END, the server deletes the temporary file.
APPEND uses the same READY handshake as WRITE. The command line is
APPEND <path> <size>\n where <path> is the file to append to and
<size> is the number of bytes to append. The file must already exist.
Client: APPEND <path> <size>\n
Server: READY\n
Client: DATA <chunk_len>\n<raw bytes>
Client: DATA <chunk_len>\n<raw bytes>
...
Client: END\n
Server: OK <bytes_appended>\n.\n
The handshake, DATA/END chunking, and error handling are identical to WRITE. Write errors during the transfer phase result in connection teardown (no ERR response), just as with WRITE. The only difference is that APPEND opens the file for appending rather than creating a new file via a temporary rename.
EXEC uses the same DATA/END framing for captured command output:
OK rc=<return_code>\n
DATA <chunk_len>\n
<raw bytes>
...
END\n
.\n
The rc field in the OK line is the AmigaOS return code from the
executed command. Because synchronous EXEC blocks until the command
completes, the return code and all captured output are available when
the response begins.
EXEC ASYNC does not use DATA/END framing. It returns immediately
with OK <id>\n.\n where <id> is the daemon-assigned process ID.
No output is captured for asynchronous commands.
AREXX uses the same response framing as EXEC. TAIL uses ongoing DATA/END streaming. See ARexx and Streaming Wire Formats below for details.
Pipelining is not supported. The client MUST wait for the complete
response to a command (up to and including the .\n sentinel) before
sending the next command. The daemon processes one command at a time per
client.
Sending a second command before the first response is complete produces undefined behavior. The daemon may interpret the second command as part of the first command's input (for commands that accept multi-line input like RENAME), or it may be buffered and processed after the first response -- no guarantee is made.
Exception: TAIL and TRACE streaming. During an active TAIL stream,
the client sends STOP\n to terminate the stream, even though the
response sentinel has not yet been received. During an active TRACE
stream, the client may send STOP\n, FILTER [args]\n, or
TIER <1|2|3>\n as mid-stream commands. These are the only cases
where the client sends data before a response is complete. See
the TAIL and TRACE command specifications in protocol-commands.md for details.
The following commands handle system queries, process management, and command execution. The wire formats for these commands use three patterns already defined in this protocol:
Used by STAT, PROTECT, PROCSTAT, SYSINFO, UPTIME,
SETDATE, CHECKSUM, LIBVER, ENV, CAPABILITIES,
TRACE STATUS: the payload consists of key=value lines in a fixed
order, one per line, subject to dot-stuffing. PROTECT returns
protection=<hex> in both read mode (path only) and set mode (path
with hex value) -- set mode applies the value, then reads it back.
Used by DIR, PROCLIST, ASSIGNS, VOLUMES, TASKS, DEVICES: the payload consists of lines with tab-separated fields, subject to dot-stuffing.
PORTS uses one port name per payload line (no tabs), dot-stuffed.
VERSION returns a single text payload line (amigactld <version>),
dot-stuffed.
Used by DELETE, MAKEDIR, RENAME, QUIT, PING,
SIGNAL, KILL, COPY, SETCOMMENT, SETENV, ASSIGN,
REBOOT, SHUTDOWN, TRACE ENABLE [func ...],
TRACE DISABLE [func ...],
TRACE TIER <1|2|3>: the response is OK\n.\n on success or
ERR <code> <message>\n.\n on failure. No payload lines.
REBOOT requires a CONFIRM keyword (like SHUTDOWN) and calls
ColdReboot() after sending OK Rebooting\n.\n -- the connection is
lost immediately. TRACE ENABLE/DISABLE toggle global_enable when
called with no function arguments, or toggle individual per-patch
enabled flags when called with function names. TRACE TIER sets the
content-based filtering tier level (1=basic, 2=detail, 3=verbose).
See protocol-commands.md for per-command semantics.
READ and EXEC (synchronous) use DATA/END chunked binary framing
as described in the READ Response and EXEC Response sections above.
EXEC ASYNC does not use binary framing -- it returns
OK <id>\n.\n with no payload.
See protocol-commands.md for the specific fields and semantics of each command.
AREXX, TAIL, and TRACE use the following wire format patterns:
AREXX uses DATA/END binary framing for the result string, identical
to EXEC. The OK status line includes rc=<N> where N is the ARexx
return code. See protocol-commands.md for details.
TAIL uses an ongoing DATA/END streaming response. Unlike READ
(where the total size is known upfront) or EXEC (where the command
completes before the response begins), TAIL's response has no
predetermined end. The stream is terminated by the client sending
STOP\n, after which the server sends END and the sentinel. During
the stream, the server may send DATA chunks at any time (when the
monitored file grows). The receiver must be prepared for an indefinite
stream of DATA chunks interspersed with arbitrary delays.
If the server encounters an error during the stream (e.g., file
deleted), it sends ERR <code> <message>\n.\n, terminating the stream.
TRACE (both START and RUN) uses the same ongoing DATA/END streaming
pattern as TAIL. Each DATA chunk contains a single tab-separated event
line with 7 fields:
<seq>\t<time>\t<lib>.<func>\t<task>\t<args>\t<retval>\t<status>.
The <status> field is a single character: O (success), E (error),
or - (neutral/void). The TRACE START stream is terminated by the
client sending STOP\n. The TRACE RUN stream auto-terminates when the
launched process exits (a # PROCESS EXITED rc=<N> comment is sent
before END); the client may also send STOP\n for early termination.
If the atrace module is unloaded during streaming, the server sends a
comment line (# ATRACE SHUTDOWN) as a DATA chunk, followed by END and
the sentinel. See protocol-commands.md for the full TRACE command specification.
FILTER is a mid-stream command sent during an active TRACE session (similar to STOP). It updates the server-side event filters without restarting the stream. The syntax mirrors TRACE START's filter arguments:
FILTER [LIB=<name>] [FUNC=<name>] [PROC=<name>] [ERRORS] [ENABLE=<func>[,func...]] [DISABLE=<func>[,func...]]
A bare FILTER with no arguments clears all filters (returns to
unfiltered mode). LIB=, FUNC=, PROC=, and ERRORS update the
per-session filters and behave identically to their TRACE START
counterparts.
ENABLE=<func>[,func...] and DISABLE=<func>[,func...] toggle
daemon-level patch state. ENABLE=FindPort,GetMsg re-enables the
specified function patches; DISABLE=FindPort disables one.
WARNING: ENABLE= and DISABLE= modify global patch state
(g_anchor->patches[].enabled). They affect ALL connected trace
clients, not just the sender. This is fundamentally different from
LIB=, FUNC=, and PROC=, which are per-session filters. Unknown
function names are silently ignored.
FILTER is fire-and-forget: the server does not send an OK\n.\n
response. Instead, after updating the filter state, the server emits a
# filter: tier=<tier>, <desc> comment as a DATA chunk to confirm the
change. The client MUST NOT attempt to read an OK/ERR response after
sending FILTER -- doing so would misinterpret the next DATA chunk as a
response line.
TIER is a mid-stream command sent during an active TRACE session (similar to FILTER). It sets the daemon's tier level for content-based event filtering:
TIER <1|2|3>
Tier levels: 1 = basic (default; suppresses noise like OpenLibrary v0
probes), 2 = detail, 3 = verbose. Like FILTER, TIER is fire-and-forget:
no OK/ERR response is sent. If the tier level changes, the server emits
a # tier changed: <name> comment as a DATA chunk. Invalid values
(outside 1-3) are silently ignored. TIER may also be used as a normal
command outside a trace stream via TRACE TIER <1|2|3>, which responds
with OK\n.\n (see the Simple OK/ERR section above).
During a TRACE stream, the daemon sends metadata as comment DATA
chunks -- DATA chunks whose payload begins with # (hash, space).
Comments are not trace events; they carry session metadata, diagnostics,
and state-change notifications. Clients SHOULD parse comments
separately from tab-separated event lines.
Known comment formats:
| Comment | When sent |
|---|---|
# atrace v<N>, <timestamp> |
Stream start: atrace module version and session start time |
# eclock_freq: <N> Hz |
Stream start: EClock frequency for timestamp interpretation |
# timestamp_precision: microsecond |
Stream start: timestamp unit |
# command: <cmdline> |
Stream start (TRACE RUN only): the launched command |
# filter: tier=<name>[, <desc>] |
Stream start; also after FILTER command |
# tier changed: <name> |
After mid-stream TIER command changes the level |
# enabled: <funcs> (normally noise-disabled) |
Stream start: functions re-enabled from default-off |
# disabled: <funcs> (manually disabled) |
Stream start: functions manually disabled |
# PROCESS EXITED rc=<N> |
TRACE RUN: launched process has exited |
# ATRACE SHUTDOWN |
The atrace module was unloaded during streaming |
# OVERFLOW <N> old events discarded |
Ring buffer overflow: N oldest events were overwritten (emitted once at session end) |
# WARNING: ... |
Diagnostic warning (e.g., unexpected LVO state) |
# Patched bsdsocket base 0x<addr> (<N> LVOs) |
bsdsocket per-opener patching diagnostic |
Clients SHOULD treat any DATA chunk beginning with # as a comment.
Unknown comment formats SHOULD be logged or ignored, not treated as
errors.
| Code | Name | Meaning |
|---|---|---|
| 100 | Syntax Error | Malformed command, unknown command, missing or invalid arguments, command too long |
| 200 | Not Found | File, directory, path, or ARexx port does not exist |
| 201 | Permission Denied | ACL rejection, operation not permitted (e.g., remote shutdown disabled) |
| 202 | Already Exists | Target already exists (e.g., MAKEDIR on existing directory) |
| 300 | I/O Error | Filesystem I/O failure, disk full, read/write error |
| 400 | Timeout | Operation timed out (e.g., ARexx reply not received within deadline) |
| 500 | Internal Error | Unexpected daemon error, resource exhaustion |
Error codes are stable. New codes may be added in future versions but existing codes will not change meaning.
Clients SHOULD handle unknown error codes gracefully (e.g., treat any unrecognized code as a generic error).
Most commands are single-line. A small number of commands require additional input lines after the command verb:
-
RENAME uses a three-line format: the verb line is followed by the old path and the new path on separate lines. Path lines follow the same line-ending and max-length rules as request lines. See protocol-commands.md for details.
-
COPY uses the same three-line format as RENAME: the verb line (with optional flags such as
NOCLONEandNOREPLACE) is followed by the source path and destination path on separate lines. See protocol-commands.md for details.
If the client disconnects mid-command (after sending the verb but before all required input lines), the server discards the partial command and closes the connection.
Most commands separate arguments with spaces. SETCOMMENT uses a tab character (0x09) to separate the path from the comment:
SETCOMMENT <path>\t<comment>\n
The tab delimiter is required because file comments may contain spaces. An empty comment (tab followed by nothing) clears the existing comment.
The banner line (AMIGACTL <version>) communicates the daemon version.
Since version 0.7.0, the daemon also supports a CAPABILITIES command
that returns a protocol=1.0 field for explicit protocol version
negotiation. Clients can use CAPABILITIES to discover supported
commands and protocol limits at runtime.
Clients SHOULD parse the version from the banner and use it to determine
feature availability. Unrecognized commands always produce
ERR 100 Unknown command\n.\n, so a client can safely attempt commands
from a newer protocol version against an older daemon.
| Element | Format | Dot-stuffed? | Followed by sentinel? |
|---|---|---|---|
| Banner | AMIGACTL <ver>\n |
No | No |
| Request | COMMAND [args]\n |
N/A | N/A |
| OK status line | OK [info]\n |
No | -- |
| ERR status line | ERR <code> <message>\n |
No | -- |
| Payload line | <text>\n |
Yes | -- |
| DATA header | DATA <len>\n |
No | -- |
| DATA body | <raw bytes> |
No | -- |
| END marker | END\n |
No | -- |
| READY handshake | READY\n |
No | No |
| Sentinel | .\n |
N/A | (is the sentinel) |
The following transcript shows a complete session. C: denotes bytes
sent by the client; S: denotes bytes sent by the server. \n
represents a single LF byte (0x0A).
[TCP connection established]
S: AMIGACTL <version>\n
C: PING\n
S: OK\n
S: .\n
C: VERSION\n
S: OK\n
S: amigactld <version>\n
S: .\n
C: SYSINFO\n
S: OK\n
S: chip_free=1843200\n
S: fast_free=12582912\n
S: total_free=14426112\n
S: chip_total=2097152\n
S: fast_total=16777216\n
S: chip_largest=460488\n
S: fast_largest=13036136\n
S: exec_version=40.68\n
S: kickstart=40\n
S: bsdsocket=4.364\n
S: .\n
C: EXEC echo hello\n
S: OK rc=0\n
S: DATA 6\n
S: hello\n
S: END\n
S: .\n
C: AREXX REXX return 6*7\n
S: OK rc=0\n
S: DATA 2\n
S: 42
S: END\n
S: .\n
C: TAIL RAM:server.log\n
S: OK 1024\n
S: DATA 53\n
S: [2026-02-20 14:30:01] Client connected from 10.0.0.5\n
S: DATA 37\n
S: [2026-02-20 14:30:05] User logged in\n
C: STOP\n
S: END\n
S: .\n
C: FOOBAR\n
S: ERR 100 Unknown command\n
S: .\n
C: QUIT\n
S: OK Goodbye\n
S: .\n
[TCP connection closed by server]