ProductCLISecurityDocsPricing
Sign in

The Basalt CLI.

Scan a repository or an AWS account from your terminal, check every branch before it ships, and ask the assistant what changed. It answers from your Basalt workspace and fixes a path on a new branch, one approved change at a time.

~/acme/platform
$ basalt


› what security issues appeared since the last deploy?
● List .github/workflows/*.yml
  ⎿  3 files
● Read .github/workflows/deploy.yml
  ⎿  38 lines
Deploys run on every push to production (.github/workflows/deploy.yml:4), so I'll compare main with origin/production.
● Pre-deploy check
  ⎿  block · 2 new · 1 closed vs origin/production
● Overview
  ⎿  2 critical, 1 high, 1 medium
● Findings
  ⎿  4 findings
● Finding 3f9c1e52-8a4d-4b1e-9c07-2d5e6f1a9b30
  ⎿  critical · SupportBot can be prompted to read customer-exports through read_file
● Read support_bot/tools.py
  ⎿  31 lines
● Read .cursor/mcp.json
  ⎿  24 lines
Since the last deploy (origin/production, merge-base 9c41e07), main adds 2 attack paths and closes 1.

New
1. CRITICAL  SupportBot can be prompted to read customer-exports through read_file
support_bot/tools.py:8 gives SupportBot a read_file tool with no path limit. The agent runs as role support-bot, whose policy allows s3:GetObject on "*" (infra/iam.tf:22). The last AWS scan shows the same grant on arn:aws:iam::412904118823:role/support-bot, so the path holds in the account, not only in Terraform.
2. HIGH  GITHUB_PERSONAL_ACCESS_TOKEN is stored in plain text
.cursor/mcp.json:20, passed to the MCP server github. It is now in the git history, so it needs to be rotated, not only removed.

Closed
• INTERNAL_API_TOKEN is stored in plain text: support-api now reads it from Secrets Manager.

Not from this deploy: ci-deploy trusts "AWS": "*" (infra/iam/ci.tf:7). It was already open on production.

The SupportBot path closes with two edits: scope the policy to tickets/* and confine read_file to one directory. Want me to make them on a new branch?

› yes, start with SupportBot
● Branch basalt/fix-support-bot-s3
Create branchgit switch -c basalt/fix-support-bot-s3   (from main)
  [y] yes   [a] yes, and don't ask again this session   [n] no
  Proceed? 

Example session on a sample repository. Tool labels, result lines and the approval prompt follow the CLI's real output; colors are adapted to this page.

Why not a general coding agent?

Use both. A general agent such as Claude Code or Cursor is built to write code across your repository. The Basalt CLI starts from the security context of your whole platform and keeps its changes inside limits enforced by the CLI.

Topic A general coding agent The Basalt CLI
context What you open, what it searches in the repository and what you paste into the chat. Your Basalt workspace: the attack-path graph, open findings with their evidence, and fixes grouped by root cause.
cloud IAM Not visible unless you give it credentials or paste the policies. Findings from the read-only AWS scan: roles, trust relationships and permissions evaluated across the account. The CLI itself makes no AWS calls during a conversation.
history Starts from the code as it is in the session. The workspace's scan history: the last scan, what opened and what was fixed in the past seven days, and a 12-week trend.
determinism Its conclusions are the model's reading of the code, so two runs can differ. Paths come from the scanner, not the model: syntax trees and IAM policy evaluation. Same code and configuration, same paths. The model explains and edits on top of that result.
evidence Can point to the files it read. Every finding carries file:line and, for cloud resources, the ARN. Answers cite them.
approvals Set by the tool and your configuration. Each edit, branch, commit, push and pull request is shown first and waits for y. Commits, pushes and pull requests are refused on the default branch.
deploy diff Can read git diff; which attack paths the diff opens is left to the model. basalt check --base <ref> compares the branch with the deployed ref and lists the paths it adds, closes and leaves open.

Works next to the agent you already use

The scan and the pre-deploy check also print JSON (--format json), so their results can be handed to any agent or CI job. The analysis is the same either way.

Commands

basalt
open the assistant
basalt login
link this terminal to your workspace
basalt scan .
scan a repository locally
basalt check --base REF
paths a branch adds or closes
basalt scan-aws
scan an AWS account, read-only
All commands and options →

Scan locally. Nothing leaves the machine.

basalt scan and basalt check make no network calls and upload nothing, whether or not the terminal is linked to an account.

  • Code is parsed into syntax trees. It is never imported or executed.
  • Python, TypeScript and JavaScript, Java, Rust and C++, plus Terraform and MCP configs.
  • Plain-text secrets are reported by name and location. The value is not written to the output.
  • Paths listed in .basaltignore are skipped and counted in the summary.
  • Text for people, JSON for pipelines, to stdout or to a file with -o.
terminal
# human-readable findings, fixes and inventory
$ basalt scan .

# the full graph and analysis as JSON
$ basalt scan . --format json -o basalt.json
scan-aws: the only calls it can make
sts             AssumeRole GetCallerIdentity
iam             GetAccountAuthorizationDetails GetPolicy GetPolicyVersion
s3              ListBuckets GetBucketPolicy GetBucketTagging GetPublicAccessBlock
lambda          ListFunctions GetFunctionUrlConfig
ecs             ListTaskDefinitionFamilies DescribeTaskDefinition
ec2             DescribeInstances
dynamodb        ListTables
secretsmanager  ListSecrets
# anything else, including s3:GetObject and secretsmanager:GetSecretValue,
# is refused before the request is sent. The JSON output lists every call made.

Pre-deploy check in CI

basalt check scans the working tree and the merge-base with the base branch, then reports only what this branch changes. Paths already open on the base are listed, but they do not fail the build.

0Pass, or a warning: lower-severity new paths, uncommitted or unpushed changes, a branch behind its base.
1The branch adds a path at or above --fail-on.
2The check could not run, for example outside a git repository or when the base ref is missing.

Pipelines can’t sign in through a browser: create a CI token on the CLI page of the app (shown once, named, revocable) and store it as the BASALT_TOKEN secret. It only unlocks basalt scan and basalt check; it can’t read your findings or use the assistant.

The base tree is exported with git archive and only parsed. Nothing in the repository is checked out, changed or run.

.github/workflows/basalt.yml
name: basalt
on: pull_request
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # check needs the merge-base
      - name: Install the Basalt CLI
        run: |
          curl -fsSL https://basalt.dev/api/v1/cli/install.sh | sh
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"
      - run: basalt check --base origin/${{ github.base_ref }} --fail-on high
        env:
          BASALT_TOKEN: ${{ secrets.BASALT_TOKEN }}  # CI token from the app's CLI page

The assistant

Run basalt in a repository and ask in plain language. It answers from your findings and your code, and it changes nothing until you say yes.

[r]

What it reads

From your workspace: findings with their evidence, fixes, red team campaigns and bug watch checks. From the repository: files, code search, git status and diff, a local scan and the pre-deploy check.

[w]

What it can change

Edit files, create a branch, commit, push, open a pull request with the GitHub CLI, and mark a finding open, accepted or fixed. Nothing else: it has no shell and never runs your project's code.

y/n

Every change is shown first

Before each change you see the diff, the branch name, the commit with its file stats, the push target or the pull request text, and you answer y or n. Anything other than an explicit yes is a no. Edits and new branches can be allowed for the rest of the session; commits, pushes and pull requests ask every time.

!main

Never the default branch

It refuses to commit, push or open a pull request from the default branch, so a fix always starts with a new branch you approved. It never force-pushes, merges or deploys.

Boundaries enforced in the CLI, not in the prompt

  • File access stays inside the repository. The .git directory and secret files such as .env, keys and Terraform state are never read or written.
  • Secret-looking values are redacted before anything leaves your machine, including in the diffs you approve.
  • Only git and gh are called, with fixed arguments. No shell, no force-push.

What it needs

  • A linked account: basalt login.
  • A monthly assistant budget set by your plan. It resets on the 1st, and /status shows what is used.
  • Git for branches and commits, and the GitHub CLI (gh) to open pull requests.
  • The model runs through Basalt's API. Each turn sends the conversation and what the assistant read, already redacted; conversations are not stored.
things to ask
› is this branch safe to deploy?
› why is the SupportBot path critical?
› fix the wildcard S3 policy in infra/iam.tf
› open a PR with the fixes from the last red team
Coming soon

Fixes as Linear issues

If your team prefers to fix through its own backlog, one button will create Linear issues from the fixes, with the scope already written. Not available yet.

Install

One command on macOS, Linux or Windows, or download the installer. Then run basalt login: the CLI works with any Basalt account, and the free plan is enough.

terminal
$ curl -fsSL https://basalt.dev/api/v1/cli/install.sh | sh
Open the zip, then right-click Install Basalt.command and choose Open; macOS asks you to confirm the first time. The installer sets up uv, an isolated Python tool manager, if you don’t have it.

Questions

Does my code leave my machine?

Not with scan or check: both run locally and send nothing. scan-aws talks only to AWS, using the read-only calls listed above.

The assistant is different: to answer, it sends the conversation and the parts of files it read to Basalt's API, with secret-looking values redacted. Conversations are not stored.

Can the assistant push to main?

No. Commits, pushes and pull requests are refused on the default branch, and force-push is never used. Each commit, push and pull request needs its own yes.

Do I need an account?

Only for the assistant and for commands that read your workspace, such as /findings and /fixes. Scanning a repository, the pre-deploy check and scanning an AWS account work without signing in.

What does it need to open a pull request?

Your own git remote and credentials for the push, and the GitHub CLI (gh) installed and signed in. Without gh, the pull request step stops with a note, and you can open the pull request in the browser.

Run it on your own repository.

Install it in one command and sign in with a free account.