Security model#

conda-exec runs binaries from environments it creates from conda packages. That is a trust decision. This page describes what conda-exec checks and what it delegates to conda.

Trust boundaries#

Warning

conda-exec trusts your configured channels completely. Running conda exec sometool downloads and executes binaries from those channels without additional verification. Only use channels you trust, and be careful when adding third-party channels via --channel or [tool.conda].channels in script metadata.

conda-exec trusts the conda solver and the configured package repositories. If a user runs conda exec ruff, they are trusting that the ruff package from the selected channels contains what it claims. conda-exec does not add verification beyond what conda itself provides. Channel trust, package signatures, and repository integrity are delegated to conda.

What conda-exec controls is the path after package installation: which binary is selected, how it is invoked, and what environment variables are passed to it.

Script lock trust#

Script lockfiles are trusted input. Running a script from lock data can install packages into a cached prefix, and package installation may execute conda package hooks under the current user account.

conda-exec only auto-uses generated lock data when it carries a digest that matches the current script dependency input. This prevents stale sidecars from silently overriding changed script metadata. It is not a substitute for trusting the lockfile itself: anyone who can modify a sidecar lockfile in a shared directory can also construct matching generated metadata.

Use --ignore-lock to bypass discovered lock data for a run, and only use sidecar or embedded lock data from repositories and directories you trust.

Subprocess execution#

conda-exec invokes tools using subprocess.run with a list of arguments:

subprocess.run([str(binary), *args], env=env)

On Unix and for native Windows executables (.exe), this is not a shell invocation. The binary path and all arguments are passed directly to the operating system’s process creation APIs, bypassing shell interpretation.

On Windows, .bat and .cmd launchers are batch scripts. Python may route those through cmd.exe even when shell=False, so their argument handling follows Windows batch-file rules. conda-exec prefers .exe over .bat and .cmd when multiple launchers exist, but users should treat batch-backed tools as having a weaker shell-injection boundary than native executables.

The only modification to the subprocess environment is PATH manipulation. In the default mode, the prefix’s bin/ directory is prepended to PATH so the tool can find its own dependencies. With --activate, conda’s activator is used to build environment variables such as CONDA_PREFIX.

Cache key validation#

Cache keys control which directory on disk an environment maps to. If a cache key could be manipulated to contain path traversal sequences, an attacker could potentially read from or write to arbitrary filesystem locations.

Three layers of validation prevent this:

  1. SAFE_TOOL_RE only allows [a-zA-Z0-9_][a-zA-Z0-9_.+-]*. This rules out /, .., null bytes, and other characters that have special meaning in file paths.

  2. SAFE_KEY_RE validates the complete {tool}--{hash} format, ensuring the hash portion contains only hex digits.

  3. Even if a key somehow passes both regexes, prefix_for() resolves the constructed path and verifies it is within the envs directory using is_relative_to(). This is the final safety net. On case-insensitive filesystems, Unicode normalization edge cases, or any other situation where regex validation might be insufficient, the resolved path check catches the escape.

Keys are also length-limited to 200 characters to prevent filesystem issues and potential buffer-related problems on platforms with strict path length limits.

File size limits#

When parsing PEP 723 inline metadata from script files, conda-exec checks the file size before reading. Files larger than 10 MB are skipped entirely, and parse_script_metadata() returns None.

This prevents memory exhaustion if conda-exec is pointed at a large binary file or a generated data file that happens to have a .py extension. Without this limit, reading a multi-gigabyte file into memory to search for a metadata block would be a denial-of-service risk, particularly in automated pipelines.

Atomic file operations#

Environment creation uses a temporary directory with atomic rename to prevent two categories of problems:

If the download or extraction fails midway, the temporary directory is cleaned up. No other process can observe a half-built environment at the final cache path, because the final path only appears after the atomic rename succeeds.

If two processes attempt to create the same environment simultaneously, only one rename succeeds. The other process detects the existing valid environment and uses it. This prevents corruption from concurrent writes without requiring explicit file locking, which is notoriously unreliable across platforms and filesystems.

Temporary directories are prefixed with .tmp- and are explicitly excluded from cache enumeration in list_cached(), so they never appear in --list output even if a crash leaves one behind.

No shell activation by default#

When conda-exec runs a tool, it does not compute activation environment variables by default. It only prepends the environment’s bin/ directory to PATH:

env["PATH"] = bin_dir + os.pathsep + env.get("PATH", "")

Activation mode, triggered by the --activate flag, uses conda’s activation logic to compute environment variables like CONDA_PREFIX and CONDA_DEFAULT_ENV. conda-exec does not invoke a shell activation script for the parent process.

If a package requires activate.d/ shell scripts, use a named conda environment with conda run or an explicitly activated shell instead. conda-exec’s activation mode is intended for tools that need activation environment variables, not tools that depend on shell-script side effects.

The default PATH-only mode keeps the subprocess environment small. Most CLI tools (linters, formatters, compilers) do not need activation and work correctly with just PATH. By making activation opt-in, conda-exec reduces the amount of environment rewriting done by default. Users who need activation variables (for tools that depend on CONDA_PREFIX or package-specific environment variables) can explicitly request them.

Environment isolation#

Each unique combination of specs and channels produces a separate cached environment. Environments are never shared between different spec combinations, even if one is a subset of another. conda exec ruff and conda exec ruff --with black create two independent environments.

This prevents interference between tool invocations. A tool cannot be affected by packages that were installed for a different invocation, and removing one cached environment has no effect on others.

The isolation also means that cached environments are self-contained conda prefixes. They have their own conda-meta/, their own package records, and their own binary directories. There is no shared state between environments beyond the filesystem directory that contains them.

Summary of guarantees#

Threat

Mitigation

Symlink escape from prefix

is_within_prefix() resolves and validates binary paths

Shell injection via tool name or args

subprocess.run with list arguments; native executables bypass shell parsing

Stale script lock data

Generated input digest must match current script dependency input

Path traversal via cache key

Regex validation + resolved path is_relative_to() check

Partial environment on crash

Atomic tempfile.mkdtemp + os.rename

Memory exhaustion from large scripts

10 MB file size limit on script parsing

Activation environment side effects

PATH-only mode by default, activator env vars opt-in via --activate

Cross-environment interference

Fully isolated prefixes per spec combination

Concurrent environment creation

Atomic rename with fallback to existing environment