Model or dataset
unjs/std-env avatar
unjs/std-env

std-env: small, runtime-agnostic detection for JavaScript

Runtime Agnostic Env Info

645 stars44 forksTypeScriptMIT

At a glance

What is it?
std-env answers three questions that every JavaScript tool ends up asking badly: which runtime am I in, am I in continuous integration, and am I running inside an AI coding agent. It does it with a handful of named exports, follows the WinterCG runtime keys proposal, ships a universal environment shim, and has no runtime dependencies at all.
Who is it for?
std-env fits a library or a build tool that has to run in Node, in a browser, on an edge worker, and inside somebody's CI, and that would rather not carry a detection dependency. It does not fit an application that needs to know which specific CI provider is running for a cosmetic reason, because the provider list is the library's business, not yours.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem with asking the question yourself

Every JavaScript tool eventually needs to know where it is running, and the naive way to do it is a series of feature checks that grow over time. Something for the process object, something else for the browser global, something for the worker global, and a pile of environment variable checks for the continuous integration case.

The problem with that is not the checks themselves, it is that every project writes a slightly different version of them, and the differences are where the bugs live. A tool that treats a runtime with Node compatibility as Node, and a tool that does not, will behave differently on the same machine, and the difference shows up as a bug report in one project and not the other.

std-env is the shared answer. Its description is runtime-agnostic utilities for detecting environments, runtimes, CI providers, and AI coding agents, and the readme leads with the three detections before it gets to the flags.

The scale is worth noticing. The package publishes only the built output, it declares that it has no side effects, and the bundle size badge in the header is there because size is the concern for a package that every other package might pull in. A tool like this is a dependency of dependencies, and it is designed to be small enough that nobody complains about it.

The license is MIT, the repository is under the unjs organisation, and the release history moves in step with the changes that matter to its users, with version 4.3.0 at the end of September 2026 after a release in the summer and one in the spring.

Runtime detection, and the difference between a flag and a value

The runtime detection is based on global variables, and it follows the WinterCG runtime keys proposal, which is the important detail. The proposal defines a set of global keys that a runtime publishes, so that a library can ask for a capability rather than infer it from a fingerprint.

The interface is two exports: a runtime name and an information object.

ts
import { runtime, runtimeInfo } from "std-env";

console.log(runtime); // "" | "node" | "deno" | "bun" | "workerd" ...
console.log(runtimeInfo); // { name: "node" }

The runtime value is a string, and the empty string is in the list, which tells you what happens when the runtime is not one of the known ones: the value is empty rather than undefined or a guess.

Then there are the individual named exports for the common cases, and the readme lists them: isNode, isBun, isDeno, isNetlify, isEdgeLight, isWorkerd, isFastly, isNub. The list mixes runtimes with platforms and with one more interesting case, and the reason for that is worth explaining.

isNub detects a runtime that augments Node.js rather than replacing it, and the readme says it is additive: isNode is also true and the runtime value stays node. That is a category of runtime that the older detection code did not have, because the assumption used to be that a runtime either is Node or is not.

Which brings us to the note that is the most practically useful line in the readme. isNode is also true in Bun and in Deno when they are running in Node.js compatibility mode, and if you need a strict check you should compare the runtime value against the string node instead of using the flag. That is a real trap: a library that uses isNode to decide whether it can rely on a Node API will work in a compatibility mode and then behave oddly in the native one.

The readme points at the source file for runtimes for the full list, which is the right place to look before you rely on one of the less common names.

CI providers, and a re-detection function

The provider detection works from environment variables, which is the only mechanism available, and the interface follows the same pattern as the runtime detection: a boolean, a name, and an information object.

ts
import { isCI, provider, providerInfo } from "std-env";

console.log({ isCI, provider, providerInfo });
// { isCI: true, provider: "github_actions", providerInfo: { name: "github_actions", ci: true } }

The shape of the result is deliberate. The boolean answers the question most code actually asks, which is whether it is in continuous integration at all, and the name answers the more specific question, and the information object carries the flag along with the name so that you can destructure it once and have both.

The provider value in the example is a snake case string, and the list of providers is longer than the one in the example, with the readme pointing at the source file for the full set.

There is one behaviour that stands out, and it is the presence of a function to re-run the detection. The environment variables are read once, when the module is first evaluated, and the flags and the detected values are computed from that read. In a long-running process, that snapshot is usually what you want, because a value that changes halfway through a run is surprising. But in a test suite, or in a process that starts a child environment and then wants to check it, the snapshot is wrong, and the re-detection function is the escape hatch.

Having that function is the design decision that makes the module usable in the situations where the naive inline checks are most useful, which is exactly the situation where the naive inline checks are most tempting.

AI coding agents, which is the newest thing here

The third detection is the one that has no counterpart in the older versions of this kind of library, and it is worth reading carefully because it comes with a set of rules that show the author has thought about the messiness.

The interface matches the other two.

ts
import { isAgent, agent, agentInfo } from "std-env";

console.log({ isAgent, agent, agentInfo });
// { isAgent: true, agent: "claude", agentInfo: { name: "claude" } }

The supported agents are listed in the readme, and there are thirteen of them: cursor, claude, copilot, devin, replit, gemini, codex, auggie, opencode, kiro, goose, pi, and junie. Two rules govern the names.

The first is that the environment variable can be set explicitly to say which agent it is. That is the escape hatch for a tool that wants to be told, rather than guessed at, and it is the right design for a detection mechanism because guessing is what causes the bugs.

The second is the normalisation rule. Two GitHub Copilot variants, one for the editor and one for the cloud, are both normalised to the single name copilot. That is a small decision with a real consequence: without it, every consumer of the value would need to know both spellings, and the alias list would leak into every codebase that reads the agent name. One name per agent, with the variants collapsed, means the list of agents in the documentation is also the list of values your code has to handle.

The detection is refreshed by a function as well, with the same reasoning as the provider detection.

It is worth asking why this belongs in a general-purpose environment package at all. The answer is the same as for the CI detection: tools want to change their behaviour when they are being driven by an agent rather than by a person, usually by being more verbose, or by asking before taking a destructive action, and the alternative to one shared detection is a dozen tools each guessing from a different set of environment variables.

The flags, and the universal environment shim

The flag table is the part of the library most code will actually use, and it is a list of booleans and a couple of values.

js
import { env, isDevelopment, isProduction } from "std-env";

There is a flag for whether standard output is a terminal, one for whether the browser global is available, one for whether it is in continuous integration, one for whether the terminal supports colour, one for whether the debug environment variable is set, one for whether the node environment is set to a development value or the mode variable is set to development, and one for the production equivalent. There are platform flags for Linux, macOS, and Windows, and a flag for a minimal environment, which is true when the minimal environment variable is set, or when it is in continuous integration, or in a test, or when there is no terminal.

Two values are more than booleans. The platform export carries the value of the process platform, and there are two version exports, one for the Node version string and one for the major version as a number.

That minimal flag deserves a note, because it is the most opinionated thing in the table. It is true whenever there is no terminal, which means the tool is running in a pipeline, in a test, or on a machine without a console. The readme's framing of it as a single concept is useful, because the four separate conditions it covers are exactly the four situations in which you want to suppress colour, suppress spinners, and print plain output.

Then there is the environment section, which is the part that makes the package usable on a runtime where the process object does not exist. The env export is a universal process environment that works across all runtimes, and the process export is a universal process shim that also works across all runtimes, and the node environment export gives the current value of the node environment variable, or undefined if it is not set.

The process shim is the piece that matters most in practice, because a browser bundle that references process at all will fail to build or crash at run time unless something provides it. A library that hands you a safe process object removes that class of problem from every consumer.

The build, the linting, and the release

The package manifest describes a project with modern tooling throughout, and the pieces are worth reading because they say what the maintainers care about.

The build is a single command, and the pack step runs the same one, so the published artefact is exactly what the build produces. The source is TypeScript, and the package ships the built output only, with separate entries for the ES module and for the type definitions, and a single export mapping the package root. It declares that it has no side effects, which lets a bundler drop it entirely when nothing is imported from it, and that is a meaningful claim for a package that is a dependency of many things.

The linting is unusual and worth naming. The lint command runs the linter and the formatter together, and there is a separate fix variant that runs the linter with its fix flag and the formatter. The formatter and the linter are the Oxc family of tools, and the type checking is done with the native TypeScript compiler preview rather than the standard one, which is the current direction for that project.

The test command is the interesting one, because it chains the whole quality gate: lint, then type check, then the test run with coverage. A contributor cannot get a green test run without passing the linter and the type checker, which is the arrangement that keeps a small package consistent.

The release command is the usual sequence, and it is run through the changelog generator rather than by hand. The changelog generator produces the release notes and the version bump, and then the publish and the tag push follow. The whole set of commands is run with the pnpm package manager at a pinned version, and the repository also carries a Renovate configuration for the dependency updates.

The rest of the tree is small and telling: a build configuration file, a workspace file, a test directory, a source directory, a changelog, a licence file, an agent instruction file, and two configuration files for the linter and the formatter. There is no example application and no documentation site, which is consistent with a package whose entire job fits in a readme table.

Editorial conclusion

std-env fits a library or a build tool that has to run in Node, in a browser, on an edge worker, and inside somebody's CI, and that would rather not carry a detection dependency. It does not fit an application that needs to know which specific CI provider is running for a cosmetic reason, because the provider list is the library's business, not yours. Verify first which runtime keys your target actually publishes, since the detection follows the WinterCG proposal rather than sniffing, that the environment variable overrides the library uses for agent detection are set in the places you need them, and that the flags you rely on are computed once at import time rather than re-read, because that is the behaviour you want in a build tool and the behaviour you do not want in a long-running server.

Frequently asked questions

What does the std-env package do?

It provides runtime-agnostic JavaScript utilities for detecting the current runtime, the continuous integration provider, and the AI coding agent, plus a set of environment flags and a universal environment and process shim that work across runtimes. It has no runtime dependencies.

How does std-env detect the runtime?

From global variables, following the WinterCG runtime keys proposal rather than fingerprinting. The runtime export gives a name, the runtimeInfo export gives an object, and there are individual flags such as isNode, isBun, isDeno, isWorkerd, and isNub for the common cases.

Why is isNode true in Bun and Deno in std-env?

Because both can run in Node.js compatibility mode, and the flag reflects that. The readme advises using a strict comparison against the runtime value when you need Node and nothing else, since the flag is also true in the compatibility mode.

Which AI coding agents does std-env detect?

Thirteen are listed, among them cursor, claude, copilot, devin, replit, gemini, codex, auggie, opencode, kiro, goose, pi, and junie. The environment variable can be set explicitly to name the agent, and the two GitHub Copilot variants are normalised to the single name copilot.

What does the process export from std-env do?

It is a universal process shim that works across all runtimes, so code that touches the process object does not break in a browser bundle or on a runtime without it. There is also a universal environment export for the same reason, and a separate export for the current value of the node environment variable.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. unjs/std-env on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/unjs-std-env.svg)](https://hysenlabs.com/projects/unjs-std-env)

Was this useful?