Sandboxing AI Sessions

Running an AI coding agent like Claude Code with broad permissions on your workstation is risky. Sandboxing isolates the agent from your host so it cannot read secrets outside the project, modify unrelated files, or reach arbitrary network destinations.

Why sandbox

  • Limit filesystem access to the project directory.

  • Restrict outbound network to an explicit allowlist (no exfiltration of credentials, no reaching internal infra by accident).

  • Contain destructive commands (rm -rf, package installs, daemon changes) inside a disposable environment.

  • Allow running the agent in a less-restrictive permission mode (for example --dangerously-skip-permissions) without putting your host at risk.

Nono

Nono is a lightweight sandboxing tool that runs commands under a configurable security profile. We provide a preconfigured profile (schedar) with the AppCat-recommended setup: network allowlist, filesystem scope, and credential handling.

Install

Follow the Nono installation guide for your platform.

Run Claude Code

From the directory you want to work in:

nono run --profile schedar -- claude

Nono sandboxes at the kernel level on your host and reuses your existing Claude Code configuration, so authentication, plugins, and settings carry over from your regular setup. No separate login or per-directory setup is required.

Profile

The schedar profile defines the allowed network destinations, filesystem paths, and credential passthrough for AppCat AI sessions.

Save the JSON below as $HOME/.config/nono/profiles/schedar.json (create the directory if it does not exist) so nono run --profile schedar can find it.

schedar.json
{
  "extends": [
    "claude-code"
  ],
  "meta": {
    "name": "schedar",
    "version": "1.0.0",
    "description": "Claude Code",
    "author": "Schedar"
  },
  "security": {
    "allowed_commands": [],
    "signal_mode": null,
    "process_info_mode": null,
    "ipc_mode": null,
    "capability_elevation": null,
    "wsl2_proxy_policy": null
  },
  "groups": {
    "include": []
  },
  "filesystem": {
    "allow": [ (1)
      "$HOME/go/pkg/mod",
      "$HOME/.cache/go-build"
    ],
    "read": [
      "$HOME/go/bin",
      "$HOME/.config/gh",
      "~/.local/state/claude/locks"
    ],
    "write": [
      "~/.local/share/claude/versions"
    ],
    "allow_file": [],
    "read_file": [],
    "write_file": [],
    "unix_socket": [],
    "unix_socket_bind": [],
    "unix_socket_dir": [],
    "unix_socket_dir_bind": []
  },
  "policy": {
    "exclude_groups": [],
    "add_allow_read": [],
    "add_allow_write": [],
    "add_deny_access": [],
    "add_deny_commands": []
  },
  "network": {
    "block": false,
    "allow_domain": [ (2)
      "api.anthropic.com",
      "statsig.anthropic.com",
      "claude.com",
      "platform.claude.com",
      "downloads.claude.ai",
      "nanoclaw.dev",
      "models.dev",
      "api.perplexity.ai",
      "api.workos.com",
      "*.chatgpt.com",
      "chatgpt.com",
      "*.openai.com",
      "cdn.openaimerge.com",
      "*.oaistatic.com",
      "*.oaiusercontent.com",
      "*.cursor.sh",
      "cursor.com",
      "factory.ai",
      "*.factory.ai",
      "gemini.google.com",
      "generativelanguage.googleapis.com",
      "play.googleapis.com",
      "registry.npmjs.org",
      "npmjs.com",
      "npmjs.org",
      "npm.duckdb.org",
      "*.yarnpkg.com",
      "yarnpkg.com",
      "bun.sh",
      "*.bun.sh",
      "pypi.org",
      "pypi.python.org",
      "pythonhosted.org",
      "files.pythonhosted.org",
      "bootstrap.pypa.io",
      "pypa.io",
      "crates.io",
      "index.crates.io",
      "static.crates.io",
      "static.rust-lang.org",
      "rustup.rs",
      "sh.rustup.rs",
      "rvm.io",
      "proxy.golang.org",
      "sum.golang.org",
      "pkg.go.dev",
      "golang.org",
      "goproxy.io",
      "gradle.org",
      "*.gradle.org",
      "maven.org",
      "repo.maven.apache.org",
      "apache.org",
      "eclipse.org",
      "spring.io",
      "java.com",
      "java.net",
      "nuget.org",
      "dot.net",
      "dotnet.microsoft.com",
      "packagist.org",
      "*.packagist.org",
      "packagist.com",
      "pub.dev",
      "ruby-lang.org",
      "rubygems.org",
      "rubyonrails.org",
      "haskell.org",
      "hex.pm",
      "metacpan.org",
      "cpan.org",
      "nodejs.org",
      "nodesource.com",
      "swift.org",
      "ziglang.org",
      "astral.sh",
      "tuf-repo-cdn.sigstore.dev",
      "github.com",
      "*.github.com",
      "*.githubusercontent.com",
      "*.business.githubcopilot.com",
      "gitlab.com",
      "*.gitlab.com",
      "bitbucket.org",
      "ghcr.io",
      "gcr.io",
      "*.gcr.io",
      "quay.io",
      "registry.k8s.io",
      "k8s.io",
      "docker.com",
      "*.docker.com",
      "docker.io",
      "*.docker.io",
      "production.cloudflare.docker.com",
      "*.production.cloudflare.docker.com",
      "docker-images-prod.6aa30f8b08e16409b46e0173d6de2f56.r2.cloudflarestorage.com",
      "public.ecr.aws",
      "mcr.microsoft.com",
      "dhi.io",
      "sourceforge.net",
      "launchpad.net",
      "ppa.launchpad.net",
      "*.amazonaws.com",
      "*.googleapis.com",
      "*.googleusercontent.com",
      "*.gstatic.com",
      "*.gvt1.com",
      "apis.google.com",
      "www.google.com",
      "dl.google.com",
      "play.google.com",
      "csp.withgoogle.com",
      "*.hashicorp.com",
      "hashicorp.com",
      "azure.com",
      "dev.azure.com",
      "login.microsoftonline.com",
      "packages.microsoft.com",
      "playwright.azureedge.net",
      "*.visualstudio.com",
      "visualstudio.com",
      "fastly.com",
      "challenges.cloudflare.com",
      "clerk.com",
      "vercel.com",
      "*.public.blob.vercel-storage.com",
      "supabase.com",
      "figma.com",
      "jsdelivr.net",
      "unpkg.com",
      "json-schema.org",
      "json.schemastore.org",
      "binaries.prisma.sh",
      "mise.run",
      "mise-versions.jdx.dev",
      "app.daytona.io",
      "archive.ubuntu.com",
      "security.ubuntu.com",
      "ports.ubuntu.com",
      "ubuntu.com",
      "debian.org",
      "*.debian.org",
      "alpinelinux.org",
      "dl-cdn.alpinelinux.org",
      "archlinux.org",
      "centos.org",
      "fedoraproject.org",
      "apt.llvm.org",
      "packagecloud.io",
      "docs.crossplane.io",
      "docs.appcat.ch",
      "kb.vshn.ch"
    ],
    "open_port": [],
    "listen_port": [],
    "connect_port": [],
    "custom_credentials": {},
    "upstream_proxy": null,
    "upstream_bypass": []
  },
  "env_credentials": {
    "github_token": "GH_TOKEN"
  },
  "environment": null,
  "workdir": {
    "access": "none"
  },
  "hooks": {},
  "rollback": {
    "exclude_patterns": [],
    "exclude_globs": []
  },
  "open_urls": null,
  "allow_launch_services": null,
  "allow_gpu": null,
  "allow_parent_of_protected": null,
  "interactive": false,
  "skipdirs": [],
  "packs": [],
  "command_args": [],
  "unsafe_macos_seatbelt_rules": []
}
1 Host directories to expose inside the sandbox with read+write access. Use the sibling read or write arrays if you only need narrower access. Re-run nono run after editing.
2 Outbound network allowlist. Add any project-specific endpoint the agent needs (private registries, additional doc sites). Anything not listed is blocked.

GitHub token

The profile’s env_credentials block injects a GitHub token into the sandbox as GH_TOKEN, so gh and other tools can authenticate without exposing your host credentials. Nono reads the token from your system keystore at runtime; see the credential injection docs for details.

  1. Create a fine-grained personal access token scoped to the repositories you work on. Grant read-only permissions; the sandboxed agent should not be able to push, open PRs, or modify repository state on your behalf.

  2. Store the token in the keystore under the name github_token:

    # macOS
    security add-generic-password -s "nono" -a "github_token" -w "<token>"
    
    # Linux (libsecret)
    secret-tool store --label="nono" service nono account github_token

    See the credential injection docs for other backends.

GH_TOKEN is then available inside the sandbox on the next nono run.