Class: RubynCode::Hooks::SubprocessExecutor

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/hooks/subprocess_executor.rb

Overview

Spawns external hook commands and exchanges JSON with them.

Protocol (matches Claude Code):

1. Spawn the command with { env, chdir: project_root }.
2. Write one JSON line to stdin:
     { "hookEventName": "PreToolUse",
       "sessionId": "...",
       "toolName": "bash",          // when applicable
       "toolInput": { ... },        // when applicable
       "prompt": "user text..." }   // when applicable
3. Close stdin.
4. Read stdout until EOF or timeout. Parse as JSON.
   - One JSON object spanning the whole output, OR
   - Newline-delimited JSON (first parseable line wins).
5. Stderr is captured and logged but not parsed.

The executor is stateless — each call spawns a fresh process. This is intentional: hooks must not keep state between invocations, and process startup cost (~30ms on macOS) is negligible compared to typical tool execution time.

Defined Under Namespace

Classes: ExecutionError, TimeoutError

Constant Summary collapse

DEFAULT_TIMEOUT =

seconds

60

Instance Method Summary collapse

Constructor Details

#initialize(project_root:, default_timeout: DEFAULT_TIMEOUT) ⇒ SubprocessExecutor

Returns a new instance of SubprocessExecutor.

Parameters:

  • project_root (String)

    working directory for spawned processes

  • default_timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    fallback timeout when a hook entry does not specify its own



41
42
43
44
# File 'lib/rubyn_code/hooks/subprocess_executor.rb', line 41

def initialize(project_root:, default_timeout: DEFAULT_TIMEOUT)
  @project_root = project_root
  @default_timeout = default_timeout
end

Instance Method Details

#run(command:, payload:, args: [], env: {}, timeout: nil) ⇒ Hash

Runs a single hook command with the given event payload.

Parameters:

  • command (String)

    executable path

  • args (Array<String>) (defaults to: [])

    arguments (rarely used; settings.json typically embeds everything in the command string)

  • env (Hash<String, String>) (defaults to: {})

    additional environment variables

  • payload (Hash)

    the JSON payload (must include :hookEventName)

  • timeout (Integer, nil) (defaults to: nil)

    per-call timeout override

Returns:

  • (Hash)

    the parsed JSON response from stdout (empty hash if no output)

Raises:



57
58
59
60
61
62
63
64
65
# File 'lib/rubyn_code/hooks/subprocess_executor.rb', line 57

def run(command:, payload:, args: [], env: {}, timeout: nil)
  timeout ||= @default_timeout
  env = default_env.merge(env)

  stdout, _stderr, = invoke(command, args, env, payload, timeout)
  parse_response(stdout)
rescue Timeout::Error => e
  raise TimeoutError, "Hook command '#{command}' timed out after #{timeout}s: #{e.message}"
end