Nushell Modules and Overlays: Package Commands Without Polluting a Session
Design Nushell modules with explicit exports, predictable imports, submodules, and overlays that can be activated, scoped, inspected, and hidden.
Nushell modules package custom commands, aliases, constants, extern declarations, submodules, and environment setup into reusable units. use imports module definitions into a namespace; overlays build on modules by activating and deactivating layers of definitions on demand. The two features solve related but different problems: modules define a reusable public surface, while overlays let a session switch between sets of names and environment configuration.
This distinction helps prevent a common dotfiles problem: loading every project helper into the default namespace and leaving all of it active for the lifetime of an interactive shell. A module can expose only the definitions it intends to support, and an overlay can activate those definitions only while the user is working in the relevant context. Neither feature is a process sandbox or a security boundary; use a separate process and operating-system controls to isolate untrusted code.
Choose a module layout that matches the public surface
A simple module can live in a file named project.nu. A directory-form module uses project/mod.nu and can organize related submodules beneath that directory. The module name comes from the file or directory. Directory form is often easier to extend because commands can be grouped into topic modules without placing every implementation in one large file.
# project/mod.nu
export def status [] {
'project status is available'
}
export def clean-cache [--dry-run] {
if $dry_run {
'would clean the project cache'
} else {
'perform a narrowly scoped cache cleanup here'
}
}
def resolve-project-root [] {
$env.PWD
}
Only definitions marked with export are available to users of the module. An unexported helper remains internal. This is valuable for keeping the public API small: consumers should depend on stable command names and arguments, not private parsing helpers or implementation details. A change to an internal definition can then remain local to the module.
Do not put destructive work directly in a command definition that is intended only to demonstrate module loading. A production clean-cache command should validate the target, provide --dry-run or ShouldProcess-like preview behavior if appropriate, and never assume the current directory is the intended project root. The sample above leaves actual deletion deliberately unspecified.
Make import patterns explicit
The use command takes a module path and an optional import pattern. You can import a particular definition, a list of definitions, or the module’s exports. The form you select determines which names enter the caller’s namespace and whether module subcommands appear as a command prefix.
use ./project status
status
For a directory-form module located through the module path, the caller can import the directory module by name. Use an explicit path when the dependency is local to a repository and a module name when it is intentionally installed and discoverable through NU_LIB_DIRS. Keep the search path controlled: a module name resolved from an unexpected directory can load different code than the caller intended.
Module lookup happens when Nushell parses the use command. A cd on the same command line does not change which relative module path is resolved, so change directories first and import on the next command line. Prefer a stable absolute path or a deliberately configured library path in automation rather than relying on the user’s current directory.
An unqualified import and a glob import are different contracts. use project imports the module’s main command and top-level members according to the module exports; use project * imports all selected members into the current namespace. A selective import limits name collisions and makes the dependency list auditable. When a public module is used in a library, document the recommended import form rather than asking every consumer to infer it.
Treat exports as API decisions
Use export def for public commands, export alias for intentional aliases, export const for public constants, and export-env for environment setup that should run when the module is used. An export-env block is a side effect, so use it narrowly and document exactly which variables it changes. Do not use environment setup as a hidden way to mutate a caller’s PATH or credentials.
module project {
export def main [] {
'project command namespace'
}
export module diagnostics {
export def check [] {
'diagnostics are ready'
}
}
export-env {
load-env { PROJECT_MODULE_ACTIVE: 'true' }
}
}
The main export can supply the module’s primary command name when imported as a module. Submodules must be exported deliberately. export module makes a submodule available as a member, while export use can re-export imported definitions as members of the parent module. These choices affect how callers import the API and should be tested with the exact import pattern you document.
Since Nushell 0.114, importing a module without * does not implicitly import exported submodule commands in the former way. Callers that need those commands should explicitly import the submodule or use the module’s documented glob pattern. This is a meaningful compatibility point for pre-0.114 modules: inspect release notes and update imports intentionally rather than assuming all nested exports arrive automatically.
Use overlays for session activation
An overlay is a layer of command, alias, and environment definitions that can be activated and hidden. The default overlay is named zero. overlay list shows which overlays are present and active. overlay use activates definitions from a module, and overlay hide removes that layer from active lookup while leaving the overlay record available for later reuse.
overlay list
overlay use project
overlay list
# Use module commands while the project layer is active.
project status
overlay hide project
An overlay’s definitions are associated with that overlay. New commands defined after activating it are recorded in the last active overlay. If project helpers should remain pristine, create a separate scratch overlay before adding local custom commands; overlay new scratchpad is designed for this case. This prevents an ad hoc helper from becoming part of a shared project module’s session layer.
Overlays can also be used within a scope. If an overlay is activated inside a do block, it is removed when that scope ends. This is useful for a temporary command namespace or test fixture that should not remain active in the user’s interactive session. Inspect the overlay list before and after the block to verify the expected scope behavior.
With overlay use --prefix, a module’s commands can remain behind a prefix rather than being imported as direct command names. The prefix applies to commands and aliases, not environment variables. This can reduce command-name collisions between modules, but the environment behavior still needs to be considered separately.
Hide is not uninstall or rollback
Hiding an overlay changes which definitions are active; it does not delete the module files from disk. The overlay remains listed as inactive and can be activated again. It should not be treated as a package uninstall, nor as a way to undo arbitrary external side effects performed by a command while the overlay was active.
overlay hide --keep-custom preserves custom definitions added to an overlay according to Nushell’s documented behavior. Use that option only when the goal is to keep local definitions after hiding the imported layer. If a cleanup action is required, name and manage the specific custom definitions or use a fresh scratch overlay with a known lifecycle.
An overlay does not revoke capabilities from a process or prevent a command from reading files, launching external programs, or modifying remote state. It is an interactive namespace and environment feature, not a sandbox. Review module code before importing it, keep library directories trusted, and execute untrusted code in an isolated process with operating-system-level controls.
Reload and test modules deliberately
The Nushell module documentation recommends starting a new shell before importing an updated directory-form module. Within one session, Nushell can continue using the version first imported even after the module files change. That behavior can make edits appear ineffective and lead an author to debug the wrong source revision.
Test a module from a clean shell with a controlled NU_LIB_DIRS, explicit import pattern, and a minimal overlay stack. Verify which names are exported, whether the module’s main command is available, and whether its submodules require explicit import. Test that export-env changes only the intended values and that hiding an overlay restores the expected active command set.
For a reusable package, include a small smoke test that imports the module by its documented path and exercises a read-only command. Test the current stable Nushell version and the oldest version the project claims to support, especially around module import behavior. Avoid relying on undeclared nested exports or on the caller’s startup files to supply hidden dependencies.
Modules establish reusable ownership boundaries; overlays control when a layer participates in command lookup. Use explicit exports for stable APIs, selective imports to limit collisions, and scoped overlays for temporary session behavior. Treat the namespace lifecycle as configuration, not isolation, and restart the shell when testing changed module files so that the code under test is unambiguous.
Related:
- Nushell Environment Scope: PATH, Closures, and External Conversions
- Nushell 0.114 Keeps Structured Shell Pipelines Moving Toward 1.0
Sources: