Create your own
Lesson illustration

Node.js Host vs. Target Process Responsibilities

Hello again. In the previous lesson, you created two separately checked and built programs: a Node.js ESM host and a bundled Frida agent that can execute only after injection into a target process. That build boundary is useful because it reflects a real runtime boundary.

This lesson turns that separation into an architectural rule. You will decide where a capability belongs based on what it needs to observe, when it must run, and what resources it owns. The goal is not merely to make Frida work; it is to keep the future CLI, REPL, probe registry, tracing, and recorder modular as the tool grows.


One tool, two execution contexts

Frida works by placing a JavaScript runtime inside the authorized target process. Your Node.js application remains outside it and controls the session through Frida’s Node binding. The two sides can communicate, but they do not share JavaScript objects, memory references, or Node.js services.

Frida’s architecture separates the controlling host tool from the injected agent inside the target app. The host and agent exchange serialized messages across Frida’s transport, while the agent alone uses GumJS to inspect and intercept the target process.

The distinction has practical consequences:

  • The host has your terminal, CLI and REPL loop, Node.js APIs, configuration, application services, and recording files.
  • The agent has access to the target’s loaded modules, native addresses, threads, registers, arguments, return values, and memory.
  • The transport between them carries data, not live object references. A NativePointer in the agent is meaningful only in that target process. If the host needs to see it, the agent must serialize it, usually as a string plus contextual metadata.

Frida’s own overview captures this model: JavaScript is injected into the target where it can access memory and hook or call native functions, while a bidirectional channel connects it to the controlling application.

Welcome | Frida • A world-class dynamic instrumentation toolkit

Read this short Frida overview to ground the architectural distinction in Frida’s runtime model: the controlling application uses a language binding such as Node.js, while the JavaScript agent executes inside the target.

In the section “Why a Python API, but JavaScript debugging logic?”, read the whole section. Focus on the injected runtime, then note the separate bidirectional communication channel described immediately afterward. Substitute Node.js for Python in the example: the separation is the same.

A useful mental model is that the agent is a small in-process sensor and actuator, while the host is the long-lived application and control plane. The agent sees native execution as it happens. The host turns selected observations into user-visible, durable, and testable behavior.


The core allocation rule: proximity to native execution

The simplest reliable rule is this:

Put code in the agent when it must touch target-process state or run at the moment a native event occurs. Put code in the host when it coordinates user intent, application state, external resources, or long-lived output.

Consider a function interception. The moment a target thread enters CreateFileW, only code executing inside that process can inspect that call’s native argument slots, examine its current thread ID, or safely read the UTF-16 string addressed by a pointer. The hook callback therefore must be agent code.

By contrast, a user typing trace-api CreateFileW is interacting with the Node.js REPL. Parsing the command, checking whether the selected instrumentation session permits a new probe, producing help text, and reporting a friendly failure are host responsibilities.

The division is not about which side has “more logic.” The host will eventually contain substantial application logic. It is about where the logic can execute correctly and safely.

CapabilityPrimary locationWhy
Parse CLI arguments and REPL inputHostRequires terminal I/O and command registry
Select a target PID and create a Frida sessionHostOwns user intent and the Node.js Frida binding
Read the bundled agent.js artifactHostRequires Node.js filesystem access
Find a loaded module at its runtime base addressAgentModule layout and addresses belong to the target process
Resolve a native export to an addressAgentThe result is a target-process address
Install and remove Interceptor listenersAgentHooks execute against target-process instructions
Decode pointer arguments, strings, handles, and return valuesAgentRequires access to target memory and call context
Apply lightweight sampling at a hot hookAgentAvoids creating unnecessary cross-process traffic
Receive trace events and validate their outer message shapeHostIt is the boundary into the application
Maintain session state and probe metadataHostMust be visible to CLI, REPL, tests, and recording
Render tables, diagnostics, and interactive outputHostOwns terminal presentation
Append NDJSON recording data to diskHostOwns durable storage and redaction policy
Decide what an event means to the domainHost or pure shared codeDomain meaning should not depend on GumJS or Node.js APIs

The last row is important for the architecture you are building. “Host versus agent” is not the only boundary. Your domain model should remain independent of both runtime environments. A trace event’s meaning—for example, “a call to CreateFileW completed with this outcome”—should be expressible as plain immutable data. The agent gathers native facts; the host converts accepted data into application and recording behavior.


What the agent can do that the host cannot

The Frida JavaScript API exposed to the injected agent includes Process, Module, Memory, Thread, and Interceptor. These are not remote Node.js objects. They act within the target’s address space.

For the Windows-focused tool in this course, agent-side work will include:

  1. Inspecting current process state. The agent can determine its target PID and architecture, enumerate modules, and find an export in the modules actually loaded by that process.

  2. Installing in-process instrumentation. Interceptor.attach() receives a target address and callbacks that run when a native thread enters or exits the function.

  3. Reading native data carefully. An argument may be an integer, pointer, handle, nullable string pointer, or structure pointer. Safe decoding must occur where the memory is addressable: inside the target.

  4. Capturing call-context information. Thread IDs, return addresses, CPU context, Windows last-error values, and return values are meaningful at specific interception phases and are available to the agent callback.

  5. Emitting compact observations. After decoding only the needed data, the agent sends a serializable event to the host.

The official API reference shows why these responsibilities are necessarily local: Process can enumerate modules and memory ranges, and Interceptor attaches to a native target address with callbacks invoked on target calls.

JavaScript API | Frida • A world-class dynamic instrumentation toolkit

Use these focused reference sections to identify the APIs that make an operation inherently agent-side. You are not expected to memorize every method; concentrate on the location and lifecycle of the data each API accesses.

In “Process, Thread, Module and Memory”, read the “Process” and “Module” subsections, from Process.id through the module export lookup methods. Notice that Process.enumerateModules() and module export lookup operate on what is loaded right now in the target. Then read “Instrumentation”, subsection “Interceptor”, from Interceptor.attach through its “Performance considerations” discussion. Focus on the callback timing, the per-invocation this object, and why frequent send() calls need restraint.

Two subtleties deserve attention.

Runtime addresses are agent-local

On modern Windows, a module is not guaranteed to load at the same base address on every execution. Address Space Layout Randomization is one reason. The host may hold a symbolic request such as:

kernel32.dll!CreateFileW

but the agent resolves that request into the actual address in the attached target. The host should not cache a raw pointer and attempt to reuse it in another process or later session.

Instead, model the host’s request with stable identifiers:

type ProbeRequest = Readonly<{
  probeId: string;
  moduleName: string;
  exportName: string;
}>;

The agent can resolve this request during installation, retain the returned listener handle for removal, and report a structured result. The raw address may be included in diagnostic metadata as a string, but it is not a durable domain identity.

Hook callbacks must stay deliberately small

An interception callback runs in a sensitive location: it is executing because the target program made a native call. If the target calls that function thousands of times per second, every extra allocation, string read, stack trace, or cross-process message contributes overhead.

This does not mean “put all processing in the agent.” It means:

  • Decode only the fields required by the active trace specification.
  • Bound pointer-based string reads.
  • Avoid expensive work for events that will be discarded.
  • Sample or batch data when tracing a high-frequency function.
  • Send plain, compact data to the host.
  • Keep persistence, formatting, aggregation, and user-facing policy in the host.

The agent should be able to be unloaded without leaving behind a hidden second application architecture inside the target.


What belongs in the host

The host owns the user’s workflow and the tool’s durable state. It is where your existing TypeScript CLI/REPL architecture continues to matter.

A host-side command handler may perform work such as:

  1. Receive a typed add-probe command from either the one-shot CLI parser or the REPL.
  2. Check that a target session has been selected and is in a state that allows instrumentation.
  3. Convert the command into an application request such as ProbeRequest.
  4. Ask a Frida-backed adapter to request installation from the injected agent.
  5. Update the host-owned probe registry only after a successful result.
  6. Render the resulting probe ID or a structured diagnostic.

Notice what this avoids: the domain and application layers do not import Frida directly, and the agent does not know about command parsing, terminal rendering, or recordings. The Frida session adapter is an infrastructure detail on the host side; it bridges an application port to a particular injected agent.

A narrow application-facing port might look like this:

export interface ProbeInstaller {
  install(request: ProbeRequest): Promise<InstallProbeResult>;
  remove(probeId: string): Promise<RemoveProbeResult>;
}

The interface belongs to the host application boundary because its callers are command handlers and session use cases. A Frida-specific adapter implements it by talking to the injected agent. Tests can implement it with a fake, without requiring Frida or a Windows target process.

This separation supports the later vertical slices cleanly:

Planned sliceHost responsibilityAgent responsibility
Process discoveryInvoke the Frida Node binding, sort/filter results, render and select a PIDNone required for initial local process enumeration
Function interceptionManage user requests and registered probe stateResolve export, attach hook, capture native call data
Windows API tracingSelect a declarative trace specification and manage lifecycleDecode API-specific arguments, return values, and call context
Session recordingValidate, redact, buffer, persist, and replay eventsEmit compact trace observations
CLI and REPLParse, dispatch, cancellation, completion, outputNone

Process discovery is deliberately an exception worth remembering. Enumerating local OS processes through Frida’s Node binding is a host operation. Enumerating modules inside the already attached target is an agent operation. Both are “discovery,” but they occur in different runtime contexts and serve different domain concepts.


Communication is a contract, not a shortcut around the boundary

The host and agent need two complementary communication patterns:

  • Control requests and replies for operations such as “install this probe,” “remove this probe,” or “report agent capabilities.” Frida RPC is a natural fit when the host expects a result.
  • Asynchronous observations for events such as “the hooked function was called” or “the agent encountered a decoding failure.” Frida’s send() mechanism is appropriate here.

The agent can also receive messages from the host using recv(). Regardless of direction, treat every message as serialized, untrusted boundary data. A TypeScript type on one side is not runtime validation on the other.

JavaScript API | Frida • A world-class dynamic instrumentation toolkit

Read Frida’s communication section to distinguish asynchronous event delivery from RPC-style control operations. This is the transport boundary your host adapter will use without leaking Frida details into application handlers.

In “Communication between host and injected process”, read from recv() through the end of the section, stopping before “Timing events”. First compare recv() and send() as message primitives. Then study the RPC export model and the Node.js consumption example immediately below it. Focus on the fact that the host loads a script, listens for messages, invokes exported operations, and remains responsible for the session.

Here is the intended shape of a probe installation interaction:

Host application handler
  calls a host-side ProbeInstaller port.

Frida-backed host adapter
  invokes an agent control operation through the active script.

Injected agent
  resolves the requested module and export, then attaches an interceptor.

Agent hook callback
  emits a compact trace message when the function is invoked.

Host message adapter
  receives the message and passes accepted event data into application services.

This is a sequence of responsibilities, not a chain of shared calls. Each boundary is a serialization and failure boundary:

  • The host adapter can fail because the session detached or the script is unavailable.
  • The agent can fail because the module or export is absent in this target.
  • A hook callback can encounter a null pointer, unreadable memory, or an unexpected native value.
  • The host can reject malformed data, decline to record an oversized event, or report an application-level error.

Later lessons will formalize the message protocol and validate it. For now, preserve the boundary by exchanging plain objects composed of JSON-compatible primitives. Do not attempt to send NativePointer, Error, Map, callback functions, or application service instances across it.


Ownership and lifecycle are split deliberately

A session crosses both runtimes, but the host should be the authoritative owner of its lifecycle.

The host knows whether the user has selected a PID, whether the session is attached, whether recording is active, and whether a detach request has been issued. It must respond when the target exits or Frida reports a detach. This is necessary because only the host can update the REPL prompt, stop a recorder, close files, and render an explanation.

The agent has a narrower local lifecycle:

  • It owns hook listener handles created by Interceptor.attach().
  • It removes individual hooks when instructed.
  • It may clear all local hooks when explicitly told to clean up.
  • It cannot guarantee cleanup if the process terminates abruptly, because it disappears with that process.

This gives a useful ownership rule:

The host owns the session model; the agent owns the in-process resources created for that session.

Avoid the opposite design, where an agent silently keeps the “real” probe registry and the host tries to infer its state from console output. The host needs an explicit model so that list-probes, remove-probe, recording, detach, and recovery after errors have predictable behavior.

Idempotence will matter. If a target process ends while the user also requests detach, both paths may attempt cleanup. A host-side session transition such as “attached” to “detached” must safely tolerate duplicate notifications. Similarly, an agent operation to remove a probe should have a defined response if the probe has already been removed.


A decision checklist for every new feature

When deciding where new code should live, apply these questions in order:

  1. Does it require a target virtual address, pointer dereference, CPU register, thread context, or exact call timing?
    It belongs in the agent.

  2. Does it require Node.js APIs, terminal I/O, filesystem access, configuration files, or durable storage?
    It belongs in the host.

  3. Does it run for every intercepted call?
    Keep it minimal and agent-side only when proximity to the call is essential. Send reduced data to the host.

  4. Must its state remain meaningful after the script unloads or the target exits?
    The host owns that state.

  5. Does it express domain meaning independently of Frida and Node.js?
    Put it in a pure shared module or host-side domain/application code, with no runtime-specific imports.

  6. Can it be tested without a running target process?
    Design it behind a host application port and test it with a fake. Keep the small genuinely native portion in the agent.

A pure shared module is appropriate for items such as event kind strings, serialization-safe message shapes, or discriminated-union tags. It must not import node:fs, frida, Process, Interceptor, or NativePointer. Shared code is a contract library, not a back door between the runtimes.


Boundary failures to recognize early

Several tempting designs create fragile behavior:

TemptationWhy it failsBetter allocation
Read target memory from the hostThe host does not execute in the target address spaceAsk the agent to decode the required bounded data
Use the agent to write recording filesCouples target instrumentation to host storage policy and Node assumptionsSend observations; record in the host
Parse REPL commands inside the agentCouples native instrumentation to terminal syntax and makes testing awkwardParse and dispatch entirely in the host
Send every raw hook argument unchangedPointers are process-local; payloads can be unsafe, huge, or meaninglessDecode selected values and serialize explicit fields
Store only probe state in the agentState vanishes on unload or process terminationKeep host registry state; let the agent retain only local hook handles
Perform large formatting or aggregation in callbacksAdds latency and overhead to target executionEmit compact data and format or aggregate in the host
Import a host service into the agentThere is no Node.js host object inside the injected runtimeDefine a plain message or RPC contract instead

A small amount of duplication is often healthier than a false abstraction. For example, the host may have a ProbeRequest model, while the agent has a local representation optimized for resolving an address and creating an Interceptor listener. They may share a plain data contract, but they should not share Frida-specific implementation classes.


Key takeaways

You now have a practical rule for locating code in the architecture:

  • The agent performs target-local work: module and export resolution, memory-safe decoding, interception, and minimal event emission.
  • The host performs application work: commands, session state, lifecycle coordination, protocol entry, rendering, recording, configuration, and persistence.
  • The transport carries serialized data, never shared pointers, functions, or service objects.
  • The host owns the long-lived session model; the agent owns short-lived local instrumentation resources.
  • For high-frequency instrumentation, gather only what must be gathered inside the agent and move interpretation, storage, and presentation to the host.

Next, you will create a deterministic x64 Windows fixture: a controlled executable that exports a hookable function and invokes the Windows APIs needed by later integration checks. That fixture will give you a safe target on which to exercise this host–agent split.

Can't find a good explanation? Sign up and we'll make it for you

Sign up