Skip to content
nerlo.ai

The nerlo CLI

Our command-line client for the Nerlo registry: search scanned packages, read their scorecards, install a package into your runtime, gate a build on what your project already uses, and submit new repositories for scanning.

Install

Now available. nerlo 0.3.0 is published on PyPI. Install it with pipx install nerlo. The distribution is named nerlo, not nerlo-cli, which is the GitHub repository name.

Already have it? Upgrade with pipx upgrade nerlo — or pip install --upgrade nerlo / uv tool upgrade nerlo depending on how you installed it. Run nerlo version to confirm which build you are on. Full instructions for every platform, including how to check what is current, are on install and upgrade the CLI.

Before you start: you need Python 3.11, 3.12, or 3.13 on Linux, macOS, or Windows. The Docker tab needs no local Python at all.

Pick your platform below. pipx (recommended everywhere it's available) keeps the CLI in its own environment, separate from your system Python. Every tab pairs the install command with the nerlo version command that confirms it worked. A Troubleshooting section follows the tabs.

It is worth confirming before you go further, and note that version is a command, not a flag:

nerlo version
nerlo 0.3.0

Install pipx so the CLI lives in its own isolated environment, then install nerlo:

sudo apt install pipx
pipx ensurepath
pipx install nerlo
Verify:
nerlo version

Troubleshooting

pipx: command not found (older Ubuntu without a pipx package): install it via pip instead.
sudo apt install python3-pip
python3 -m pip install --user pipx
pipx ensurepath
PEP 668 error (externally-managed-environment): use the pipx steps above. Never pass --break-system-packages. It risks breaking your OS's own Python. See the PEP 668 note below.

Fallback: dedicated virtualenv

Prefer an explicit venv? That works too:

python3 -m venv ~/.local/nerlo
~/.local/nerlo/bin/pip install nerlo
ln -s ~/.local/nerlo/bin/nerlo ~/.local/bin/nerlo

Troubleshooting

Landed here from a copy-pasted error? Find your exact message below.

error: externally-managed-environment (PEP 668)

Modern Debian/Ubuntu, Fedora, and Homebrew Python refuse pip install into the system interpreter under PEP 668. Install with pipx instead. Do not pass --break-system-packages. It can corrupt your OS's Python. See the Linux (Debian/Ubuntu) or your platform's tab above for the pipx steps.

pipx: command not found / nerlo: command not found

The tool installed but its directory isn't on your PATH yet. Run pipx ensurepath and open a new terminal.

  • Debian/Ubuntu (older): no pipx package: Linux (Debian/Ubuntu) tab shows the pip bootstrap.
  • Arch: PATH not picked up in fish/zsh: see the Linux (Arch) tab.
  • macOS: Homebrew bin differs by chip (/opt/homebrew/bin vs /usr/local/bin): see the macOS tab.
  • Windows: add %USERPROFILE%\.local\bin and %APPDATA%\Python\Python312\Scripts: see the Windows tab.

cannot be loaded because running scripts is disabled on this system (PowerShell execution policy)

PowerShell blocked the pipx install script. Run this one-time step, then retry the install from the Windows tab:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

apk: unable to select package pipx (Alpine has no pipx)

Alpine's repositories don't ship pipx. Use the dedicated-venv path on the Linux (Alpine/generic) tab: apk add python3 py3-pip py3-virtualenv then create a venv.

Consumer commands

Search the registry

We match your keyword across name, description, and author (2–100 characters):

nerlo search "weather"
nerlo search "weather" --json

The ID column is the package's server UUID, and every command that addresses a package takes it: nerlo rescan uses it directly, and nerlo info and nerlo install accept it alongside a package name or a skill id.

Inspect a package

Score, badge, artifact type, and per-scanner scoresheets for one package. Address it by name, by skill id, or by the UUID from nerlo search:

nerlo info weather-mcp
weather-mcp (88417784-98a2-488b-bd25-23213377d135)
  repository: https://github.com/lamcearber-spec/weather-mcp
  type:       mcp_server
  badge:      Caution
  score:      91.38
  installed via Nerlo: 0 total (0 in last 30d, CLI installs only)

  per-scanner scoresheets:
SCANNER                    SCORE  BADGE    FINDINGS
agentshield                100.0  Unrated
cisco-skill-scanner        100.0  Unrated
agent-audit-kit            68.0   Unrated  5
nerlo-behavioral           99.5   Unrated  1
nerlo-install-instruction  100.0  Unrated
capslock                   100.0  Unrated
trivy                      100.0  Unrated
osv-scanner                100.0  Unrated
trivy_image                100.0  Unrated
govulncheck                100.0  Unrated

The per-scanner table is the point: eleven scanners ran, one scored 68 while the rest scored at or near 100, and the composite of 91.38 is one weighted view of that spread rather than a replacement for it. installed via Nerlo counts CLI installs only and is an engagement signal, not an endorsement.

We show every scanner's verdict side-by-side and label the composite as one view among many — on the registry website and, as of this release, in the CLI.

No findings does not mean no risk. Nerlo aggregates automated scanner output; automated scanning is one layer of defence, not a substitute for review.

Install a package

We write an mcpServers config entry for the target runtime. You need an API token; name the package however you like — a name, a skill id, or a UUID:

nerlo install weather-mcp-server-2bdf07f --target claude-code --token $NERLO_API_TOKEN
Installed weather-mcp-server-2bdf07f -> ~/.claude.json
  note: no runnable package source detected — entry records the repository; finish the command wiring for your platform.

--target takes claude-code, cursor, gemini, or mcp. Installing the same package twice is refused unless you pass --force.

Install respects the composite badge: Clean proceeds, Caution asks you to confirm, Flagged is refused, and an Unrated package — one nobody has scanned — is not installable either. Those are display names for a scan result, not a guarantee about a package — the CLI is reporting what the scanners returned.

The API is unchanged and still uses the original three values, so ?badge=, any JSON you parse, and the CLI's own terminal output take Verified for Clean, Caution for Caution, and Unsafe for Flagged.

nerlo install sends anonymous install telemetry and says so the first time it runs. Opt out with NERLO_TELEMETRY=0 in your environment, or telemetry=false in ~/.nerlo/config.

Check a project against the registry

check is the gate. It reads the same platform configs nerlo install writes, resolves every entry against the registry, and exits non-zero when policy is violated — so it fails a build whether or not anybody remembers to look at a dashboard.

With no PATH it scans your per-user locations. Give it a PATH and it scans a project checkout instead, which is the CI case:

nerlo check
nerlo check .
nerlo check . --fail-on caution
nerlo check . --json
STATUS   ARTIFACT     PLATFORM  SCORE  SCANNERS  SOURCE
FLAGGED  weather-mcp  mcp       81.3   11        .mcp.json

1 flagged

FAIL: 1 artifact(s) violate --fail-on flagged.

The split is deliberate: a CI runner's $HOME belongs to an ephemeral machine, so mixing it into a repository's gate would make the result depend on the runner rather than on the repository.

Exit codes

"Could not determine" is never a pass, at any --fail-on level. Handle 3 explicitly in your pipeline. A bare set -e will fail the build on it, which is the right outcome, but it will report it as if a package had been flagged — and "we could not check" is a different fact from "we checked and it is bad".

Policy levels

--fail-on selects the threshold and defaults to flagged. The words you type are the display words, and they line up with what the registry shows:

Already have --fail-on unsafe in a pipeline? Leave it. It is the retired spelling of the same level and the CLI accepts it permanently — this release changed which word is documented, not which builds pass. Anything parsing --json is untouched too: the fail_on and status fields still carry the original wire words.

any means "fail unless the registry affirmatively verified this". It is the right setting once a team has submitted its dependency set and wants to keep it that way; it is not the default because the registry does not yet list every package in the ecosystem, and a gate that red-builds everything on day one gets deleted in week one.

A package the registry has never seen reports as UNKNOWN with a pointer to nerlo submit. Unknown is not a pass — it is nobody having looked.

Continuous integration

The three snippets below are the same gate. Each one treats "could not determine" as its own outcome rather than letting set -e fold it into a generic failure.

GitHub Actions

name: Nerlo
on: [pull_request]

jobs:
  nerlo-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - run: pipx install nerlo
      - name: Gate on the registry's verdict
        shell: bash
        run: |
          set +e
          nerlo check . --fail-on flagged
          status=$?
          set -e
          case "$status" in
            0) echo "Nerlo: every configured package passed." ;;
            1) echo "::error::Nerlo: a configured package violates the policy."; exit 1 ;;
            3) echo "::error::Nerlo: could not determine a verdict. This is not a pass."; exit 1 ;;
            *) echo "::error::Nerlo: usage error (exit $status)."; exit 1 ;;
          esac

GitLab CI

nerlo-check:
  image: python:3.12-slim
  before_script:
    - pip install nerlo
  script:
    - |
      set +e
      nerlo check . --fail-on flagged
      status=$?
      set -e
      case "$status" in
        0) echo "Nerlo: every configured package passed." ;;
        1) echo "Nerlo: a configured package violates the policy."; exit 1 ;;
        3) echo "Nerlo: could not determine a verdict. This is not a pass."; exit 1 ;;
        *) echo "Nerlo: usage error (exit $status)."; exit 1 ;;
      esac

Jenkins

stage('Nerlo') {
  steps {
    script {
      def status = sh(script: 'nerlo check . --fail-on flagged', returnStatus: true)
      if (status == 0) {
        echo 'Nerlo: every configured package passed.'
      } else if (status == 1) {
        error 'Nerlo: a configured package violates the policy.'
      } else if (status == 3) {
        error 'Nerlo: could not determine a verdict. This is not a pass.'
      } else {
        error "Nerlo: usage error (exit ${status})."
      }
    }
  }
}

Machine-readable output

--json prints the same result as structured data, with exit_code carried in the payload so a downstream step does not have to re-derive it:

nerlo check . --json
{
  "scope": "project",
  "path": ".",
  "fail_on": "unsafe",
  "fail_on_label": "flagged",
  "exit_code": 1,
  "summary": {
    "total": 1,
    "unsafe": 1,
    "caution": 0,
    "withheld": 0,
    "unscored": 0,
    "unknown": 0,
    "unresolved": 0,
    "error": 0,
    "verified": 0
  },
  "unreadable_configs": [],
  "artifacts": [
    {
      "name": "weather-mcp",
      "platform": "mcp",
      "artifact_type": "mcp_server",
      "source": ".mcp.json",
      "status": "unsafe",
      "badge": "Unsafe",
      "score": 81.3,
      "server_id": "e32f8cdc-d2e5-4acd-8c37-09c2dc245a11",
      "scanners": 11,
      "duplicate_matches": 8,
      "note": ""
    }
  ]
}

What the CLI cannot gate yet

check gates what your project consumes. It cannot yet gate what you publish: there is no way to submit your own repository and block a build on the resulting verdict. nerlo submit queues a scan and returns immediately — it has no --wait, nothing polls for completion, and nerlo info does not carry a verdict in its exit code. If you need that today, you would be building the polling loop yourself against the registry API.

Publisher commands

Submit a repository

We queue your repository for ingestion and scanning (authenticated):

nerlo submit https://github.com/you/your-mcp-server --token $NERLO_API_TOKEN

Add --type to state the artifact kind explicitly — mcp_server, claude_skill, gemini_extension, or cursor_rule. Omit it and the server infers it from the repository.

Request a re-scan

By server UUID — the ID column from nerlo search (authenticated):

nerlo rescan 88417784-98a2-488b-bd25-23213377d135 --token $NERLO_API_TOKEN

The command's help also offers a skill slug, and that path works as well. The UUID is shown because nerlo search prints it, so it is the value already in front of you.

Tokens

install, submit, and rescan read --token or the NERLO_API_TOKEN environment variable. search, info, and check need no token. We issue tokens per account; treat them like passwords, and in CI put them in your platform's secret store rather than in the workflow file.

Configuration

Point the CLI at a different registry with --api-url or NERLO_API_BASE_URL (defaults to https://api.nerlo.ai). Every command that talks to the registry accepts the flag — that is all of them except version, which talks to nothing.