Skip to content

Protocol Reference

Extensions communicate with runok using JSON-RPC 2.0 over standard I/O. runok spawns the extension as a child process, writes a single request to its stdin, then reads a single response from its stdout.

PropertyValue
ProtocolJSON-RPC 2.0
Transportstdio (stdin/stdout)
EncodingUTF-8
Directionrunok writes to extension’s stdin, reads from extension’s stdout
LifecycleOne request per process invocation (spawn, request, response, exit)

Extension stderr is discarded by runok (redirected to /dev/null). Use stderr for debug logging in your extension without affecting the protocol.

runok sends a single JSON-RPC 2.0 request with method validateCommand.

{
"jsonrpc": "2.0",
"id": 1,
"method": "validateCommand",
"params": {
"command": "curl",
"flags": {
"X": "POST"
},
"args": ["https://api.example.com"],
"raw_command_line": "curl -X POST https://api.example.com",
"env": {
"AWS_PROFILE": "prod"
},
"cwd": "/home/user/project"
}
}
FieldTypeDescription
commandstringThe base command name (e.g., "curl", "git")
flagsobjectParsed flags as key-value pairs. Boolean flags have empty string values.
argsstring[]Positional arguments (non-flag tokens)
raw_command_linestringThe original command line as entered by the user
envobjectRelevant environment variables (key-value pairs)
cwdstringCurrent working directory where the command would be executed

Flags are extracted from the parsed command. For example, curl -X POST --silent produces:

{
"flags": {
"X": "POST",
"silent": ""
}
}

Flags are parsed into key-value pairs. The flag name (without leading dashes) becomes the key.

  • For flags with a value (e.g., -X POST or --request POST), the value is stored as a string.
  • For boolean flags without a value (e.g., --silent), the value is an empty string ("").

The extension must write a single JSON-RPC 2.0 response to stdout.

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"status": "deny",
"message": "POST requests are not allowed in production",
"fix_suggestion": "curl -X GET https://api.example.com"
}
}
FieldTypeRequiredDescription
statusstringYesOne of "allow", "deny", or "ask"
messagestringNoHuman-readable message explaining the decision
fix_suggestionstringNoA suggested alternative command
StatusAction
"allow"The command is permitted to run
"deny"The command is blocked. message and fix_suggestion are shown to the user.
"ask"The user is prompted for confirmation before the command runs

Any unrecognized status value is treated as "ask".

Extensions can return a JSON-RPC 2.0 error object to indicate a protocol-level failure:

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32600,
"message": "Invalid request: missing required field 'command'"
}
}

runok treats any JSON-RPC error response as an InvalidResponse error, which triggers the ask fallback.

runok defines three error categories for extension communication:

ErrorCauseBehavior
TimeoutExtension did not respond within the configured timeoutFalls back to ask
SpawnExtension process failed to start (e.g., binary not found)Falls back to ask
InvalidResponseExtension returned unparseable output or a JSON-RPC errorFalls back to ask

In all error cases, runok falls back to ask mode, prompting the user for confirmation. This ensures that a broken or unreachable extension never silently allows a dangerous command.

When verbose mode is enabled, error details are printed to stderr:

[verbose] Extension error: timeout after 5s
[verbose] Extension error: spawn error: plugin not found
[verbose] Extension error: invalid response: JSON parse error: ...

Extension output is sanitized before display. ASCII control characters (except \n, \r, \t) are stripped to prevent terminal injection attacks from malicious extension output.

The extension process is specified via the executor field in the configuration. runok uses shell-style tokenization (shlex) to parse the command:

types:
InternalUrl:
executor: 'deno run --allow-net ./checks/url_check.ts'

The executor string is split into a program and arguments. In this example, runok spawns deno with arguments ["run", "--allow-net", "./checks/url_check.ts"]. Quoted strings and backslash escapes in the executor command are handled correctly.