nono

Overview

In 2026, the trend of AI sandboxes started taking off, and by now, it’s no longer surprising to see so many of them appearing. However, it’s important to understand that each sandbox has its own pros and cons, especially since the definition of a “sandbox” can sometimes blur. How is it designed? What can it do? What can’t it do? How does it protect you? For now, consider a sandbox to be a tool that runs a program in a restricted environment. Regardless of the underlying approach, its primary job is to prevent malicious programs from executing directly on our host machine. Even Docker has its own sandboxing mechanism called Docker Sandbox. Then there’s Firecracker, created by AWS, which utilizes microVMs so that it doesn’t share a kernel like Docker does and there are many more.

There is a new AI sandbox that caught my attention when it first launched. It’s called nono. By leveraging kernel-level enforcements like Landlock for Linux and Seatbelt for macOS, nono creates a tightly restricted execution environment for a process. At first glance, it feels similar to existing sandboxing tools, but over time, it has introduced interesting features that set it apart, such as:

  • Credential injection, which they call Phantom Tokens (you can find more details here). While other sandboxes might implement something similar, I’ve only found it in nono so far. If anyone knows of another sandbox with this feature, please let me know! :)
  • File attestation. If you’ve ever used Sigstore, you might be familiar with this concept. It’s a great idea to implement this in an AI sandbox to prevent your instruction files from getting tampered with.
  • Generating a profile from an executed file. I actually had a similar idea and wanted to request this feature, but never got the chance to write the feature request. But nono did it! They introduced a new nono learn flag in version 0.3.0.

In this blog post, I’ll be exploring the nono learn feature and how to use it effectively.

Getting Started with nono

At the time of writing, to install nono on a Debian-based system, you have to download and compile the source code, as no official .deb package is available yet. To make the installation process easier for everyone, I actually raised a PR to add a Debian packaging step to the nono repository.

Once installed, you can simply run nono -h to see the help message.

nono -h
CLI for nono capability-based sandbox

USAGE
  nono <command> [flags]

GETTING STARTED
  setup      Set up nono on this system

CORE USAGE
  run        Run a command inside the sandbox
  shell      Start an interactive shell inside the sandbox
  wrap       Apply sandbox and exec into command (nono disappears)

EXPLORATION & DEBUGGING
  learn      Trace a command to discover required filesystem paths
  why        Check why a path or network operation would be allowed or denied

SESSION MANAGEMENT
  rollback   Manage rollback sessions (browse, restore, cleanup)
  audit      View audit trail of sandboxed commands
  trust      Manage file trust and attestation

POLICY & PROFILES
  policy     Inspect policy groups, profiles, and security rules
  profile    Create and manage nono profiles

OPTIONS
  -s, --silent         Silent mode - suppress all nono output (banner, summary, status)
      --theme <THEME>  Color theme for output (mocha, latte, frappe, macchiato, tokyo-night, minimal) [env: NONO_THEME=]
  -h, --help           Print help
  -V, --version        Print version

LEARN MORE
  Use `nono <command> --help` for more information about a command.
  Read the docs at https://nono.sh/docs

If you’ve followed nono since its early days, you’ll immediately notice some significant changes after just a few months the project is evolving rapidly.

To understand exactly what nono learn does, we can start by checking its help menu.

nono learn -h
Trace a command to discover required filesystem paths

USAGE
  nono learn [flags] <program>...

OPTIONS:
  -p, --profile <NAME>  Use a named profile to compare against (shows only missing paths)
  -s, --silent          Silent mode - suppress all nono output (banner, summary, status)
      --json            Output discovered paths as JSON fragment for profile
      --theme <THEME>   Color theme for output (mocha, latte, frappe, macchiato, tokyo-night, minimal) [env: NONO_THEME=]
      --timeout <SECS>  Timeout in seconds (default: run until command exits)
      --all             Show all accessed paths, not just those that would be blocked
      --no-rdns         Skip reverse DNS lookups for discovered IPs
  -v, --verbose...      Enable verbose output
  -h, --help            Print help


EXAMPLES
  nono learn -- my-app                         # Discover paths needed by a command
  nono learn --profile my-profile -- my-app    # Compare against an existing profile
  nono learn --json -- node server.js          # Output as JSON for profile
  nono learn --timeout 30 -- my-app            # Limit trace duration

PLATFORM NOTES
  Linux   Uses strace (install with: apt install strace)
  macOS   Uses fs_usage (requires sudo)

It comes as no surprise that it relies on strace under the hood on Linux. If you’ve ever used strace for debugging or performance analysis, you know it’s the standard tool for tracing system calls and is incredibly useful for seeing exactly what a process is touching.

Reference: What problems do people solve with strace?

Unlike Docker, nono doesn’t create an entirely new, heavy environment. It doesn’t spin up a full container, drop you into a different shell, or manage the process space in a drastically different way. Instead, you just prepend nono to your regular commands. The process still runs on your host machine and can access your files and network, but only within the strict boundaries of the security policy you’ve set.

The core idea behind nono learn is to help us automatically generate a restrictive profile by observing a command’s behavior. Imagine you need to execute a command, but you aren’t entirely sure what it does. With software supply chain attacks becoming increasingly common these days, it’s healthy to be skeptical and avoid blindly trusting executables. But here’s the catch:

How do you know what a command requires without running it first?

This is exactly where nono learn comes in handy. It allows us to profile the application’s needed filesystem paths and network access without having to guess, and because it can also trace within a sandbox, we don’t need to worry as much about potential damage while building that profile.

We can run a quick test to see how a profile is generated in practice.

nono learn --json -- cat /etc/hosts
WARNING: nono learn runs the command WITHOUT any sandbox restrictions.
The command will have full access to your system to discover required paths.

Continue? [y/N] y

nono learn - Tracing file accesses and network activity...

127.0.0.1 localhost
127.0.1.1 trinity

# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
{
  "filesystem": {
    "allow": [],
    "read": [
      "/etc"
    ],
    "write": []
  },
  "network": {
    "outbound": [],
    "listening": []
  }
}

To use these paths, add them to your profile or use --read/--write/--allow flags.

The output generates a simple profile fragment for the cat /etc/hosts command. Notice, however, that it only identified /etc as a read path rather than /etc/hosts specifically. If you look back at the help menu, you might remember the --all flag. Adding that flag changes the output considerably.

nono learn --all -- cat /etc/hosts
...

============================================================
 nono learn - Discovered Paths
============================================================

 READ (1 paths)
 ----------------------------------------
  /etc

 i 5 paths already covered by system defaults

============================================================

It turns out, the reason /etc/hosts wasn’t explicitly listed in the first run is because it’s already covered by the system’s default profiles. But how do we know what those default profiles actually contain?

Getting the List of Profiles

First, it helps to locate the profiles currently available on the system.

nono policy profiles
nono policy: 13 profiles

  Built-in:
    claude-code      Anthropic Claude Code CLI agent            extends default
    codex            OpenAI Codex CLI agent                     extends default
    default          Default conservative base profile
    go-dev           Go SDK development profile with GOPATH and module support extends default
    node-dev         Node.js SDK development profile with nvm, fnm, pnpm, and npm support extends default
    openclaw         OpenClaw messaging gateway                 extends default
    opencode         OpenCode AI coding assistant               extends default
    python-dev       Python SDK development profile with pyenv, conda, and pip support extends default
    rust-dev         Rust SDK development profile with cargo and rustup support extends default
    swival           Swival CLI coding agent                    extends default

  User (~/.config/nono/profiles/):
    data-processing  Read from input, write to output
    example-agent    Template for creating custom agent profiles
    offline-build    Build environment with no network access

From here, we can examine the content of the default profile.

nono policy show default
nono policy: profile 'default'

  Description:  Default conservative base profile

  Security groups:
    deny_credentials
    deny_keychains_macos
    deny_keychains_linux
    deny_browser_data_macos
    deny_browser_data_linux
    deny_macos_private
    deny_shell_history
    deny_shell_configs
    system_read_macos
    system_read_linux
    system_write_macos
    system_write_linux
    user_tools
    homebrew
    dangerous_commands
    dangerous_commands_macos
    dangerous_commands_linux
  Signal mode:   Isolated

Inspecting the Profile Groups

While the profile overview doesn’t show exact file paths, it does list “Security groups”. By inspecting a specific group’s content, we can better understand the profile’s permissions. My assumption is that /etc/hosts is covered by the system_read_linux group. Checking the group explicitly verifies this assumption.

nono policy groups system_read_linux
nono policy: group 'system_read_linux'

  Description:  Linux system paths required for executables to function
  Platform:     linux
  Required:     no
...
    /etc/hosts
...

As expected, the default profile already includes /etc/hosts within the system_read_linux group. This explains why running nono learn (without --all) masks that path—it only shows paths that aren’t yet covered by your default policies.

Now, we can observe what happens when a command sends a network request to an external website.

nono learn --json -- curl https://github.com
...

{
  "filesystem": {
    "allow": [],
    "read": [
      "/etc",
      "/etc/gnutls",
      "/home/<user>",
      "/home/<user>/.config",
      "/proc/sys/crypto"
    ],
    "write": []
  },
  "network": {
    "outbound": [
      {
        "addr": "20.27.177.113",
        "port": 443,
        "count": 1
      },
      {
        "addr": "127.0.0.53",
        "port": 53,
        "count": 1,
        "hostname": "_localdnsstub"
      }
    ],
    "listening": []
  }
}

To use these paths, add them to your profile or use --read/--write/--allow flags.
Network activity detected. Use --block-net to restrict network access.

The trace detected an outbound connection to github.com at IP address 20.27.177.113, which you can easily verify using dig.

dig github.com

The second outbound connection listed (127.0.0.53:53) is simply systemd-resolved handling the DNS resolution, which we can safely ignore.

Interestingly, the generated profile also uncovered several read paths we didn’t explicitly request. For instance, /etc/gnutls is accessed to load TLS certificates required for the HTTPS connection, and the home directory paths are checked for user-level curl configurations.

This same transparent tracing applies to writes on the filesystem as well.

nono learn --json -- touch here
WARNING: nono learn runs the command WITHOUT any sandbox restrictions.
The command will have full access to your system to discover required paths.

Continue? [y/N] y

nono learn - Tracing file accesses and network activity...

{
  "filesystem": {
    "allow": [],
    "read": [
      "/etc"
    ],
    "write": [
      "/home/<user>"
    ]
  },
  "network": {
    "outbound": [],

Now, this raises an interesting question.

are-you-thinking

Much like writing YARA rules for malware analysis, we can use nono to dynamically execute a dubious command and generate a behavioral profile. We can then inspect this profile to understand exactly what the command is doing under the hood, ensuring it isn’t attempting anything malicious.

Securing Developer Systems with nono

Just over the past few days, there have been several high-profile software supply chain attacks hitting critical security and development tools, from Trivy to LiteLLM, and even Telnyx. What’s next?

antgnp

This got me thinking, could we leverage nono to mitigate the impact of supply chain attacks? Absolutely! The developer machine is arguably the most critical endpoint to protect. As AI agents rapidly evolve and integrate into our daily workflows, it’s easy for developers to lose track of what exact commands are executing behind the scenes. With nono, we can place guardrails around these executions to guarantee they aren’t compromising the system—potentially enforcing this sandboxing across an entire organization’s development workflow.

A typical implementation might start with establishing a baseline set of nono profiles for each project, installing them directly onto developers’ local environments and CI/CD pipelines. As a practical example, we can implement a simple developer workflow that automatically wraps common execution environments like python, node, npm, and pip with a secure nono baseline profile.

Create a profile for developer as below.

cat > ~/.config/nono/profiles/developer.json << 'EOF'
{
  "meta": {
    "name": "developer",
    "version": "1.0.0",
    "description": "System-wide developer sandbox profile"
  },
  "security": {
    "groups": [
      "deny_credentials",
      "deny_shell_history",
      "deny_shell_configs",
      "system_read_linux",
      "system_write_linux",
      "dangerous_commands_linux"
    ]
  },
  "filesystem": {},
  "network": { "block": false },
  "workdir": { "access": "readwrite" }
}
EOF

Next, we create a shell script and drop it into /etc/profile.d/. This script will automatically intercept target commands and invoke them securely via nono using our new profile.

tee /etc/profile.d/nono-preexec.sh << 'EOF'
if [ -n "$BASH_VERSION" ]; then
    _nono_run() {
        local prog="$1"
        shift

        # Skip if already inside nono to avoid recursion
        if [[ -n "$NONO_ACTIVE" ]]; then
            command "$prog" "$@"
            return
        fi

        # Skip if nono not available
        if ! command -v nono &>/dev/null; then
            command "$prog" "$@"
            return
        fi

        export NONO_ACTIVE=1
        nono run --profile developer --silent --allow-cwd -- "$prog" "$@"
        unset NONO_ACTIVE
    }

    # Wrap specific programs
    alias python3='_nono_run python3'
    alias python='_nono_run python'
    alias node='_nono_run node'
    alias npm='_nono_run npm'
    alias pip3='_nono_run pip3'
    alias pip='_nono_run pip'
fi
EOF

Finally, source the script to apply the changes to your current session.

source /etc/profile.d/nono-preexec.sh

To prove that the profile is actively protecting the system, we can run a Python script that deliberately tries to read a restricted path, like an SSH private key.

python3 -c "print(open('/home/<user>/.ssh/id_ed25519').read())"
/usr/lib/python3/dist-packages/apt/__init__.py:37: Warning: W:Unable to read /etc/apt/apt.conf.d/ - opendir (13: Permission denied)
  apt_pkg.init_config()
Traceback (most recent call last):
  File "<string>", line 1, in <module>
PermissionError: [Errno 13] Permission denied: '/home/<user>/.ssh/id_ed25519'
Error in sys.excepthook:
Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 228, in partial_apport_excepthook
    return apport_excepthook(binary, exc_type, exc_obj, exc_tb)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3/dist-packages/apport_python_hook.py", line 114, in apport_excepthook
    report["ExecutableTimestamp"] = str(int(os.stat(binary).st_mtime))
                                            ^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/opt/-c'

Original exception was:
Traceback (most recent call last):
  File "<string>", line 1, in <module>
PermissionError: [Errno 13] Permission denied: '/home/<user>/.ssh/id_ed25519'

You might be wondering why a Python script attempting to access an SSH key threw a permission denied error for /etc/apt/apt.conf.d/. We can tell Python to ignore previous errors using the -S command-line flag.

python3 -S -c "print(open('/home/<user>/.ssh/id_ed25519').read())"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
PermissionError: [Errno 13] Permission denied: '/home/<user>/.ssh/id_ed25519'

This PermissionError is exactly what we want to see. Because the script ran under our custom nono profile, its attempt to read the private key was successfully intercepted and blocked by the deny_credentials security group.

A workflow of implementing nono in developer machine is as below.

nono_secure_execution_flow

Rather than scan the package before execution, we can use nono to sandbox the execution and prevent malicious behavior. This is especially useful for AI agents that can execute arbitrary code and by the time we learn new attack vectors, we can update the profiles to mitigate the impact. The profile is not limited to restrict file access, it can also restrict network access to avoid exfiltrating data to the internet.