Harper v5.0 introduces a significantly upgraded JavaScript execution environment for applications. The new environment provides application-specific context and stronger security controls, and represents a meaningful step forward in both developer experience and platform security.
The Old Approach
In prior versions of Harper, application code was loaded with a straightforward native import(). There was no execution boundary: every application ran in the same global context as Harper itself, meaning modifications to built-in prototypes could affect the entire runtime. There was also no way to give each application its own context—a logger tagged to that application's name, access only to that application's configuration—without threading those values through manually.
What's New in v5.0
A VM-Based Module Loader
Harper now loads application modules through Node.js's vm.SourceTextModule API, giving each application its own module cache and execution context. This is what enables application-specific JavaScript context: each app gets its own module graph, isolated from other apps and from Harper's own internals.
The loader is configurable. The default is vm mode. If legacy code is having trouble with the new loader, you can set applications.moduleLoader: native to fall back to the old behavior while working through the migration. There's also a compartment mode using SES Compartments for applications that want the strictest possible isolation.
Application-Specific Context
When application code runs, it now has access to a set of globals and module exports scoped specifically to that application:
logger— Logs are automatically tagged with your application's name, making output from multi-application servers dramatically easier to follow and filter.config— The configuration object for your specific application, sourced from your component'sconfig.yaml. No more passing config around manually.
These are available via the harper package, which is the preferred way to access them:
import { logger, config, tables, Resource } from 'harper';
The logger you get this way is automatically tagged to your application. The config is your application's config. We strongly recommend migrating to explicit imports from harper rather than depending on globals. The globals still work for backward compatibility, but they may not always give you the fully contextualized version.
Security Controls
Harper's VM context is not a hardened security sandbox—it provides access to the full breadth of Node.js APIs, which is what makes it a powerful environment for building real server-side applications. A determined attacker with code execution could potentially find ways around the VM isolation. What we've built is a meaningful security improvement that blocks a broad class of common exploitation patterns while maintaining the full capability of Node.js. Here's what the new environment actively protects against:
Prototype pollution. The default lockdown mode (freeze-after-load) freezes JavaScript intrinsics—Object, Array, Promise, Map, Set, and others—after application code is loaded. Prototype pollution is one of the most common JavaScript exploitation techniques, and freezing the prototypes of these core types stops it cold. For even stricter protection, you can enable applications.lockdown: ses to apply full SES lockdown from the ses package. This is the gold standard for Hardened JavaScript, though it's more likely to be incompatible with packages that make assumptions about mutability of built-ins.
Malicious process spawning. One of the most dangerous things a compromised Node.js application can do is spawn arbitrary subprocesses. In v5.0, child_process's spawn(), exec(), and execFile() are intercepted and checked against an explicit allowlist in applications.allowedSpawnCommands. If a process command isn't on the list, the call throws an error. Only the commands you explicitly authorize can be launched.
There's a related improvement for legitimate subprocess use as well: Harper now requires a name option when spawning processes. Harper's multi-threaded architecture means that naive subprocess code spawns duplicate processes on each worker thread restart. Named processes are now tracked via PID files and deduplicated automatically.
Unauthorized file access. By default, applications can only load modules from within their own directory tree (allowedDirectory: app). The VM loader enforces this at module load time via path validation. This prevents an application from loading modules—or reading files—from outside its own scope.
Node.js built-in module allowlist. The applications.allowedBuiltinModules option lets you explicitly enumerate which Node.js built-in modules applications are permitted to import. This closes off access to sensitive Node APIs that most applications don't need.
Supply chain attack mitigation. Harper now uses --ignore-scripts by default when installing application packages. Install scripts are a common vector for supply chain attacks—malicious packages often hide payloads in postinstall scripts. Disabling these by default means that unless you've explicitly opted in with allowInstallScripts, package installation can't silently execute arbitrary code.
Migrating Existing Applications
For most well-behaved applications, upgrading to v5.0 should be seamless. But there are a few patterns that will run into the new environment:
If your application accesses files outside its own directory, you'll need to configure applications.allowedDirectory. Note that dev mode installs (the default when you choose the "dev" configuration during installation) set allowedDirectory: any, which allows access to any path on the filesystem, so local development shouldn't be affected. Production installs default to restricting access to the app directory.
If your application modifies intrinsic prototypes (e.g., extending Array.prototype or Object.prototype), this will break under the default freeze-after-load lockdown because those prototypes are frozen after startup. The intent with this change is to prevent the dangerous and verboten pattern of prototype mutation; thus you may need to refactor. If you're using a library that does this, you can set lockdown: none to disable it entirely while you work through the issue.
If your application spawns child processes, you'll need to add those commands to applications.allowedSpawnCommands in your configuration, and update your spawn calls to include a name in the options:
import { exec } from 'node:child_process';
exec('my-tool', ['--flag'], { name: 'my-named-process' }, callback);
If you have packages that need to spawn, and you are unable to modify them, you can disable the VM loader for dependencies with applications.dependencyLoader: native (see next section).
If the VM module loader is causing compatibility issues with specific packages, you can configure applications.dependencyLoader: native to load npm dependencies with the native module loader while still getting application context for your own code. The default auto mode already uses native loading for npm packages that don't declare harper as a dependency.
If all else fails, setting applications.moduleLoader: native disables the VM loader entirely, restoring the pre-v5 behavior for your application. This disables all the dependency loader options, directory containment, and spawn command protections (lockdown can still be used though). This is a temporary escape hatch—if you hit something that requires it, please file an issue so we can address the compatibility gap.
Our Commitment to Platform Security
The context for these changes matters. Supply chain attacks on npm packages are increasing. The npm ecosystem's size is also its vulnerability surface—malicious packages, dependency confusion attacks, and compromised dependencies are real and growing threats. When you run a Harper application, you're potentially running dozens of third-party packages, any of which could be a vector.
Our goal with v5.0's JavaScript environment is to make Harper a platform where a compromised dependency has a much harder time doing real damage. Blocked prototype pollution, constrained subprocess spawning, restricted file access, and explicit module allowlists all work together to reduce what malicious code can accomplish.
For customers running on our Fabric hosting service, there are additional layers of isolation at the infrastructure level: each tenant runs in a separate Docker container on a dedicated user account. These infrastructure controls complement the application environment hardening—defense in depth rather than a single layer.
Full SES compartmentalization is available today for applications that opt into it, and we're continuing to improve the compatibility of the VM-based loader with the broader npm ecosystem. As Node.js's module isolation primitives mature, we'll keep building on them.
Questions or issues with the v5.0 migration? Open an issue on the Harper GitHub repo or reach out on Discord.






.webp)
