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.
$ 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.
file:line and, for cloud resources, the ARN. Answers cite them.
y. Commits, pushes and pull requests are refused on the default branch.
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
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
.basaltignoreare skipped and counted in the summary. - Text for people, JSON for pipelines, to stdout or to a file with
-o.
# human-readable findings, fixes and inventory $ basalt scan . # the full graph and analysis as JSON $ basalt scan . --format json -o basalt.json
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.
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.
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.
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.
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.
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
.gitdirectory 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
gitandghare 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
/statusshows 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.
› 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
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.
$ curl -fsSL https://basalt.dev/api/v1/cli/install.sh | sh
powershell -ExecutionPolicy ByPass -c "irm https://basalt.dev/api/v1/cli/install.ps1 | iex"
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.