Hello. This module begins the point where the TypeScript CLI becomes a dynamic-instrumentation tool: it will have a Node.js host running on Windows and a separate Frida agent that runs inside an authorized target process.
Those are different runtime environments, not merely different folders. The host needs Node.js APIs, the Frida Node bindings, terminal I/O, and file access. The agent needs Frida’s in-process JavaScript APIs such as Process, Module, Interceptor, and send. Treating them as separate build targets prevents a large class of mistakes before anything is injected.
In this lesson, you will configure that separation: a strict ESM TypeScript build for the host, a Frida-typed TypeScript check for the agent, and frida-compile to bundle the agent into the single JavaScript file Frida will later inject. This is a foundation for attach, tracing, recording, and the shared CLI/REPL features that follow.
Two programs, two runtimes, one deliverable
The eventual tool has two cooperating programs:
| Concern | Node.js host | Frida agent |
|---|---|---|
| Runs in | Your Windows terminal process | The selected target process |
| Primary responsibilities | CLI/REPL, process selection, session lifecycle, persistence, rendering | In-process observation and instrumentation |
| APIs available | Node.js standard library, frida Node bindings | Frida Gum globals such as Process, Module, and Interceptor |
| Build output | ESM files under dist/host/ | One bundled script, dist/agent.js |
| Must not assume | It can access a target process’s memory directly | It has Node.js, a terminal, filesystem access, or your host application objects |
The most important operational fact is this:
Frida injects JavaScript, not TypeScript.
TypeScript is valuable because it validates the agent source before runtime and gives you accurate editor support. But before injection, the TypeScript agent must be transpiled and bundled into a JavaScript artifact.

A bundler is especially important once the agent has multiple modules. Native Frida injection expects script source, whereas your development source will eventually contain imports for argument decoders, event serializers, hook installers, and protocol definitions. frida-compile turns that module graph into an injectable script.
JavaScript API | Frida • A world-class dynamic instrumentation toolkit
Read the short “Getting started” section of Frida’s JavaScript API documentation. It explains why Frida recommends TypeScript bindings for agent development.
In the “Getting started” section, read the complete introductory paragraph beginning with Frida’s recommendation to use TypeScript bindings. Focus on the tooling benefits: the declarations are a development-time safety net, not code that is injected into the target.
A source layout that makes the boundary visible
Start with a deliberately small layout:
src/
main.ts
agent/
index.ts
dist/
host/
main.js
agent.js
tsconfig.base.json
tsconfig.host.json
tsconfig.agent.json
package.json
src/main.ts is the initial host entry point. As the application gains vertical slices, host-side code can be organized underneath src/ by feature: process discovery, sessions, probes, recording, and terminal adapters.
src/agent/index.ts is the injected agent entry point. Agent-only hook logic and Frida-specific helpers should remain under src/agent/.
This physical separation is useful, but it is not security or architectural enforcement by itself. TypeScript starts compilation from include patterns, but imported files can still enter a compilation even when they are excluded from the initial file search. Therefore, maintain this dependency rule:
- Host code may read the generated agent artifact as data.
- Agent code must not import host code.
- Agent code must not import Node built-ins such as
node:fs,node:path, ornode:readline. - Any future shared message-contract code must be pure TypeScript: no Node APIs and no Frida runtime APIs.
The next lesson will sharpen the host-versus-agent responsibility boundary. For now, the build configuration makes accidental mixing much harder.
Install the runtime and build dependencies
From the project root, install the Frida Node binding as a production dependency. The host will use it at runtime to enumerate processes, attach, create scripts, and manage sessions.
pnpm add frida
Install the compiler, Node typings, Frida Gum typings, and agent bundler as development dependencies:
pnpm add -D typescript @types/node @types/frida-gum frida-compile
The key packages have distinct roles:
fridais the host-side Node.js binding used at runtime.@types/frida-gumdeclares the globals and APIs available within a Frida agent.frida-compiletranspiles and bundles the agent for injection.typescriptperforms strict static checks.@types/nodeprovides the host’s Node.js types.
Keep ESLint, Prettier, and Vitest from the engineering baseline configured as before. They apply across the repository, but the TypeScript targets below control which runtime-specific global APIs each program can see.
Build from a strict shared baseline
Create tsconfig.base.json. This contains rules that should apply to both runtime targets.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
}
}
A few choices deserve attention:
strictis the core correctness setting. It makes nullability, implicitany, and several unsafe assumptions visible early.noUncheckedIndexedAccessis useful for CLI arguments, decoded native values, and later event payloads, where an indexed value may be absent.exactOptionalPropertyTypesprevents treating “not provided” and “provided asundefined” as interchangeable.useUnknownInCatchVariablessupports the later design in which unexpected exceptions are translated at an application boundary instead of leaking through the system.isolatedModuleskeeps every source file compatible with independent transpilation, which is appropriate when an agent bundler processes modules.verbatimModuleSyntaxmakes import behavior more explicit. This reduces surprises when maintaining ESM host code.
The shared file deliberately does not choose a module system or a type environment. Those differ between the host and the agent.
TSConfig Reference - Docs on every TSConfig option
Use the relevant TSConfig reference sections to understand why separate target configurations should inherit common strictness while defining distinct module resolution, global types, and outputs.
First, in the “Extends” section, read from the explanation that a base configuration is loaded first through the warning about circular inheritance. Focus on inheritance behavior, especially the fact that each inheriting configuration replaces include and exclude. Then read the “Module” and “Module Resolution” sections. Find the guidance that modern Node projects typically use nodenext while bundled code typically uses preserve or esnext; read the module guidance and the subsequent explanation of bundler resolution. In the “Types” section, read the whole section, including the difference from typeRoots. Use the distinction to confirm why types is appropriate when each target should receive only its intended global declarations. Finally, in “Out Dir”, read the explanation of how TypeScript preserves source structure under its output folder, from output layout.
Configure the Node.js host target
Create tsconfig.host.json:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src",
"outDir": "./dist/host",
"types": ["node"],
"sourceMap": true,
"noEmitOnError": true
},
"include": ["src/**/*.ts"],
"exclude": ["src/agent/**/*.ts"]
}
This target emits ESM JavaScript intended for a current Node.js runtime. It will compile:
src/main.ts
into:
dist/host/main.js
The "type": "module" field in package.json, shown shortly, complements NodeNext. In host TypeScript files, use Node-compatible ESM imports. Once you introduce relative host imports, write the emitted extension in the TypeScript import:
import { createApplication } from "./composition-root.js";
Although the source file is .ts, Node will execute the emitted .js file. NodeNext makes TypeScript model that runtime behavior accurately.
The host target deliberately specifies:
"types": ["node"]
This brings in Node global types such as process, Buffer, and timer APIs. It does not add Frida Gum’s in-process globals. A host should access Frida through explicit imports from the frida package, which keeps the dependency obvious:
import frida from "frida";
Later, the host will use that imported binding to attach and load the contents of dist/agent.js. It should never attempt to import the agent source as executable Node code.
Configure the Frida agent target
Create tsconfig.agent.json:
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["frida-gum"],
"noEmit": true
},
"include": ["src/agent/**/*.ts"]
}
This configuration has a different purpose from the host configuration:
- It type-checks only agent source.
- It makes Frida Gum declarations available through
@types/frida-gum. - It intentionally does not expose Node globals.
- It asks TypeScript to reason about code that a bundler will process.
- It emits no JavaScript itself, because
frida-compileowns agent bundling.
The "types": ["frida-gum"] setting is a useful boundary. In src/agent/index.ts, Frida-provided globals should be known to TypeScript:
const pid = Process.id;
send({
type: "agent-ready",
pid
});
Conversely, this should fail during agent type-checking:
process.cwd();
That failure is valuable. The injected runtime is not Node.js, so process.cwd() would not be a legitimate agent operation even if a developer could somehow suppress the compiler error.
Create this minimal agent entry point:
// src/agent/index.ts
export {};
send({
type: "agent-ready",
pid: Process.id
});
export {} makes the file an explicit module. send() and Process.id are Frida agent APIs, and their definitions come from @types/frida-gum.
This message is only an initial smoke signal. Do not treat the object shape as a durable cross-process protocol yet. A later lesson will define, validate, and version typed host-agent messages before they reach application code or recording storage.
Bundle the agent and build the host
Add or merge the following fields in package.json:
{
"private": true,
"type": "module",
"scripts": {
"typecheck:host": "tsc -p tsconfig.host.json --noEmit",
"typecheck:agent": "tsc -p tsconfig.agent.json",
"build:host": "tsc -p tsconfig.host.json",
"build:agent": "frida-compile src/agent/index.ts -o dist/agent.js",
"build": "pnpm run typecheck:host && pnpm run typecheck:agent && pnpm run build:host && pnpm run build:agent"
}
}
The build has two intentional validation stages:
- Type-check the host against Node’s runtime model.
- Type-check the agent against Frida Gum’s runtime model.
- Emit host JavaScript using TypeScript.
- Bundle the agent using
frida-compile.
Use the separate scripts while developing:
pnpm run typecheck:host
pnpm run typecheck:agent
pnpm run build:host
pnpm run build:agent
Use the complete build before committing or packaging:
pnpm run build
Add generated artifacts to .gitignore:
dist/
The expected output after a successful build is:
dist/
host/
main.js
main.js.map
agent.js
Depending on the bundler version and source-map settings, there may also be a source map for agent.js.
The agent output should be treated as an opaque deployment artifact. It is valid JavaScript intended for Frida’s injected runtime, not a Node command you should run with node dist/agent.js.
For a minimal host smoke test, create src/main.ts:
console.log("Host build succeeded.");
Then verify the host artifact separately:
pnpm run build:host
pnpm exec node dist/host/main.js
You should see:
Host build succeeded.
At this stage, building the agent proves that your agent source is typed and bundleable. Actual attachment and injection come after a controlled Windows fixture is introduced.
Diagnose the common boundary mistakes
When this setup fails, the error often tells you exactly which boundary has been crossed.
| Symptom | Likely cause | Corrective action |
|---|---|---|
Cannot find name 'Process' in src/agent/ | Frida Gum types are absent or the wrong TS config is being used | Confirm @types/frida-gum is installed and run tsc -p tsconfig.agent.json |
Cannot find name 'process' in agent code | Node-specific code entered the agent | Move the operation to the host, then expose only the required command/message boundary |
| Node cannot resolve a relative import in compiled host output | Host source omitted the .js extension in an ESM relative import | Write ./module.js in the TypeScript source import |
dist/agent.js contains unresolved imports | The agent was transpiled without bundling, or the wrong command was run | Use frida-compile for the final injectable asset |
Agent files appear in dist/host/ | The host include or exclude patterns are wrong | Keep src/agent/**/*.ts excluded from the host’s root file set |
| An excluded agent file is still compiled by the host | A host file imported it | Remove the cross-runtime import; exclude cannot block files imported into the module graph |
That last case is particularly important. Configuration supports the architectural boundary, but it cannot replace design discipline. Keep a host import graph and an agent import graph. Later in the course, automated dependency checks will make this rule enforceable.
Build-completion checklist
Before proceeding, confirm the following:
pnpm run typecheck:hostsucceeds.pnpm run typecheck:agentsucceeds.pnpm run buildcreates both host and agent artifacts.src/agent/index.tsrecognizesProcessandsend.- Node-specific globals are rejected from the agent target.
pnpm exec node dist/host/main.jsruns the host smoke test.- You do not run
dist/agent.jswith Node; it is an injection artifact.
You now have a clean dual-target foundation: strict ESM JavaScript for the Node host and a separately type-checked, bundled JavaScript agent for Frida injection. The Frida declarations improve agent correctness without making the agent a Node program, and frida-compile produces exactly the single artifact the host will later load.
Next, you will make the runtime boundary explicit: which responsibilities belong in the host, which must happen inside the target process, and how that distinction informs the application’s ports and adapters.
Can't find a good explanation? Sign up and we'll make it for you
Sign up