Skip to content

CLI reference

palisade-sec [--version] <command> [args]

Commands split into two layers. The offline core makes no network calls, needs no API key, and sends no telemetry. The judgment layer needs the [judge] extra (pip install 'palisade-sec[judge]', or uvx --from 'palisade-sec[judge]' palisade-sec ...) and calls an endpoint you configure in the environment or a .env - TypeSafe by default, or any OpenAI-compatible endpoint. Everything is MIT and free to run; the split is keyless-and-offline versus bring-your-own-endpoint. Full setup in the judgment layer guide.

CommandNeeds [judge] + a judgment endpoint?
scan, map, baseline, fix, redteam (synthesis)No - offline, keyless
auditYes - exits 2 with a hint if the extra or key is missing
reviewOnly for the AI-judged layer; runs taint-only without them, and says so on stderr
redteam --executeYes - exits 2 with a hint if the extra or key is missing

Exit codes (the contract)

CodeMeaning
0Success, or nothing new. Includes “findings exist but --ci not set” and “all findings baselined under --ci”.
1A gate tripped: scan --ci / review --ci found a new HIGH finding, redteam --execute --ci saw an attack land, or audit --ci produced a BLOCK decision.
2Usage, target or setup error: the path does not exist, an explicit --config/--rules is missing, a --ci run scanned 0 files, the judgment layer is missing its [judge] extra or key, an output path is a symlink (refused), redteam --execute --ci had attacks that errored, or a connected surface refused to connect (bad webhook, expired token, missing scope, pr with no GitHub token).
3Internal error - a bug in Palisade, not a finding. Please report it.

A scan that read 0 files never prints a green tick: it warns “Nothing was scanned … This is not a clean result.” in every output format, and under --ci it exits 2 rather than passing a gate that checked nothing (a JS/TS-only repo without the [js] extra is the usual cause).

Output files (palisade-report.md, palisade-fixes.md, palisade-review.md, .palisade/baseline.json, or an explicit --output) are never written through a symlink: a scanned repository could plant one at those names, so Palisade refuses and exits 2 instead.

palisade-sec scan [PATH]

Scan a file or directory (default .) for source → LLM → sink paths.

FlagEffect
--allShow MED/LOW findings too. Default view: HIGH + “risky” downgraded findings.
--jsonEmit the stable JSON document (below) to stdout instead of terminal output.
--sarifEmit SARIF 2.1.0 to stdout, for GitHub code scanning / any AppSec pipeline.
--reportAlso write palisade-report.md - a shareable mini threat model grouped by severity.
--ciExit 1 if any (new, when combined with --baseline) HIGH finding exists; exit 2 if 0 files were scanned.
--baseline FILEDiff against a baseline; only new findings are reported/counted. Stale entries are noted.
--rules DIRLoad additional/overriding YAML rules from a directory (same id overrides a builtin).
--config FILEExplicit config file (.palisade.toml format).
--assume-params-untrustedLibrary mode: parameters of public (non-underscore) functions become untrusted sources (param:<name> in traces).

File selection: *.py/*.pyi always; *.js/*.mjs/*.cjs/*.jsx/*.ts/*.tsx when the [js] extra is installed (otherwise skipped with a note). Always excluded: .venv, venv, site-packages, .git, build, dist, node_modules, caches, plus .gitignore patterns. tests/**, test_*.py, *_test.py, and conftest.py are skipped unless include_tests = true. Unparseable files are skipped with a warning, never a crash.

SARIF / GitHub code scanning

scan --sarif emits SARIF 2.1.0 (severity high->error, med->warning, low->note; sink as the primary location, source and LLM boundary as related locations, line-shift-resilient partialFingerprints). A five-line workflow puts findings in the GitHub Security tab:

permissions: { contents: read, security-events: write }
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uvx palisade-sec scan . --sarif > palisade.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: palisade.sarif }

palisade-sec baseline [PATH]

Fingerprint current findings so CI fails only on new ones.

FlagEffect
--output FILEBaseline path (default <PATH>/.palisade/baseline.json).
--rules DIR / --config FILEAs in scan.

Fingerprints are sha256(rule + source file + normalized source snippet + sink file + normalized sink snippet) - line-shift resilient by construction. The file is sorted and deterministic (diff-friendly); commit it. Duplicate findings collapse to one fingerprint with a count.

palisade-sec fix [PATH]

Write a remediation plan: for each finding, a rule-tailored guardrail plus a pytest asserting the guardrail blocks the canonical attack and preserves the happy path. Deterministic templates, fully offline, and the scanned project is never modified.

FlagEffect
--output FILEPlan path (default palisade-fixes.md).
--allCover MED/LOW findings too.
--rules / --config / --assume-params-untrustedAs in scan.

Guardrail families: AST allowlist (exec/eval), argv + executable allowlist (shell), single-SELECT parser check (SQL), host allowlist + private-IP block (HTTP/SSRF).

palisade-sec map [PATH]

Inventory the codebase’s AI surface: LLM call sites (with provider and model), prompts (static vs dynamic), agent tools (with the capabilities their bodies exercise), agents/chains, retrieval sites, and dangerous config flags (allow_dangerous_code=True, …). Deterministic and offline - no network, no key. It is the foundation the judgment checks build on.

FlagEffect
--jsonEmit the inventory as JSON (summary + artifacts).
--config FILEAs in scan.

palisade-sec audit [PATH] (judgment layer)

Run the semantic checks over grounded artifacts and route each to pass / review / block:

  • excessive agency - over agent tools the map found: can the tool take an irreversible action, is it gated, how much harm if a manipulated model calls it.
  • taint exploitability - over each verified source → LLM → sink finding: how realistically exploitable is that specific path, and how severe.

Every question is anchored to a fact the static analyzer verified. Needs the [judge] extra and reads the backend from the environment or .env; if either is missing it exits 2 with a clear message. An unverified (generic) backend never emits BLOCK on judgment alone - such a decision downgrades to REVIEW.

FlagEffect
--jsonEmit findings as JSON (schema_version: 1).
--ciOpt-in gate: exit 1 if any finding is a BLOCK decision.
--config FILEAs in scan.
--policy FILEPolicy YAML (thresholds and criteria). See below.

Policy: the thresholds are yours

audit and review route on thresholds you can set, rather than ones baked into the tool. Resolution order:

  1. --policy PATH
  2. .palisade/policy.yaml in the scanned tree
  3. [tool.palisade.semantic] in that tree’s pyproject.toml
  4. the built-in defaults
# .palisade/policy.yaml - a fintech's appetite
checks:
excessive_agency:
action_threshold: 0.45 # block above this probability
review_threshold: 0.20 # flag for a human between the two
gate_threshold: 0.60 # count a tool as "gated" only above this
severity_block: 1 # harm >= this turns a review into a block
taint_exploitability:
action_threshold: 0.40
severity_block: 1

A partial file overlays the defaults per check and per field, so setting one threshold does not silently reset the rest. Unknown keys are an error, not a shrug: action_treshold fails loudly with a warning and the run falls back to the defaults, rather than leaving a gate at a threshold nobody chose.

One field is confined on purpose. criteria is editable English that is interpolated into the question sent to the judge:

checks:
excessive_agency:
criteria:
irreversible: "any movement of customer money, or any write to prod"

Because that text becomes part of a model prompt, a policy file discovered inside the tree being scanned may set thresholds but not criteria - it is dropped with a warning naming the file. Otherwise a repository you scan could ship a policy reading “nothing here is ever irreversible” and argue the judge out of its own finding, which is the exact attack class this tool detects. Only --policy, where you named the file yourself, may set criteria.

palisade-sec review [PATH] (judgment layer)

Compose scan + map + the semantic checks + red-team synthesis into one prioritized report with a posture score: a number 0..100 and a named band (Critical / High / Moderate / Low), derived from the tier counts and printed with the breakdown beside it. It is a posture over detected findings (likelihood × impact), not a safety score. If no judgment backend is configured (or the [judge] extra is not installed), review runs taint-only and says why on stderr, so --json stays parseable.

review judges each finding once per run and emits both the composed posture and the audit view (audit_findings in --json) from that single pass, so a run needs only one judgment pass and audit/review never disagree within it. The model is probabilistic, so judged numbers and the posture score vary across separate runs; the score is deterministic within a run, not across runs.

FlagEffect
--jsonEmit the report as JSON (schema_version: 1), including the posture block.
--reportAlso write palisade-review.md.
--ciExit 1 on a new HIGH taint finding (baseline-diffed); exit 2 if 0 files were scanned. Gates only on deterministic taint findings: review’s judged signals never gate. Their calibration is preliminary: measured on a 10-case seed corpus (n=4 to 6 per signal), not a benchmark result; the judged layer stays advisory. For an explicit gate on judged decisions, opt in with audit --ci.
--baseline FILEBaseline to diff --ci against.
--config FILEAs in scan.
--policy FILEPolicy YAML, as in audit. Only consulted when a backend is configured.

palisade-sec redteam [PATH]

Synthesize a targeted adversarial attack suite from the AI System Map, and optionally execute it against a live target you own.

Default is advisory and OFFLINE: it generates attacks aimed at the discovered tools, prompts, and agents (tool coercion, instruction override, data exfiltration, jailbreak, system-prompt leak) but does NOT run them.

FlagEffect
--jsonEmit the suite (or, with --execute, the run report) as JSON.
--variants NAttack variants per target (1-5).
--executeFire the suite at a live target. Requires --approve.
--approveRequired with --execute: you authorize firing adversarial inputs.
--target URLTarget endpoint (or set PALISADE_REDTEAM_TARGET; key via PALISADE_REDTEAM_KEY).
--ciWith --execute: exit 1 if any attack lands; exit 2 if attacks errored.
--config FILEAs in scan.

Execution drives the endpoint you provide, in your environment - Palisade never executes your code. It scores landed attacks with the judgment backend from .env (deterministic tool-invocation checks plus a model for behavioral judgment), so --execute needs the [judge] extra and a configured backend. Run it only against systems you own and are authorized to test.

palisade-sec connect github|slack|llm

Set Palisade up from the terminal; credentials go to your OS keychain (with the keyring extra) or a 0600 file. Full guide: connect.

CommandNotes
connect github [--token T] [--no-gh]Reuses the gh CLI’s token when logged in, else the OAuth device flow. Verified before storing.
connect slack [--webhook URL] [--no-test]Incoming webhook; posts a test message first.
connect llm --provider typesafe|anthropic|openai_compatible [--key K] [--endpoint U] [--model M] [--workspace-id ID] [--no-verify]For audit / review only. --workspace-id is required for an Anthropic organization key, which is not scoped to a workspace; a key created inside a workspace needs no flag. Also readable as ANTHROPIC_WORKSPACE_ID.
connectionsWhat is connected, from where, redacted.
disconnect github|slack|llmRemove stored credentials.

The environment always wins over stored values, so CI is unchanged.

palisade-sec pr [PATH]

Opens a draft pull request containing the fix plan. Options: --repo owner/name (default: the git remote), --base, --branch, --plan-path, --baseline, --no-draft, --dry-run. The branch name is derived from the findings, so re-running updates the same pull request. Exits 0 when there is nothing to open one for, 2 when GitHub is not connected.

palisade-sec notify [PATH] --slack

Scans and posts a Block Kit summary to the connected webhook. Options: --baseline (post only new findings), --link URL (button target), --dry-run (print the message instead of sending it). Nothing is posted without this command.

Judgment configuration

audit, review, and redteam --execute read their backend from environment variables, or from a .env in the current working directory (see .env.example, or the template in the judgment layer guide). The process environment wins over .env. Keys are never logged.

VariableMeaning
PALISADE_JUDGE_BACKENDtypesafe (default) or openai_compatible.
PALISADE_JUDGE_ENDPOINTBase URL. Defaults to the TypeSafe API for typesafe; required for openai_compatible.
PALISADE_JUDGE_MODELModel id. Defaults to jev-latest for typesafe; required for openai_compatible.
TYPESAFE_API_KEYKey for the typesafe backend.
PALISADE_JUDGE_API_KEYKey for the openai_compatible backend.

Install the extra with pip install 'palisade-sec[judge]'. TypeSafe answers are treated as verified; a generic OpenAI-compatible endpoint is validated against a strict schema and treated as best-effort/unverified, so it can never BLOCK or raise a Critical posture on judgment alone. Calibration of the judged signals is preliminary: measured on a 10-case seed corpus (n=4 to 6 per signal), not a benchmark result; the judged layer stays advisory.

Inline suppressions

Some findings cannot be fixed today: the code is genuinely sandboxed, the risk is accepted, or the fix is scheduled. Without a way to silence one, “one unfixable finding disables the tool” and the whole scanner gets removed from CI. Suppress it in place instead:

exec(code) # palisade: ignore[PI-EXEC] - runs in a locked-down sandbox
eval(code); // palisade: ignore[PI-EXEC] - input is schema-validated upstream
FormEffect
# palisade: ignore[PI-EXEC]silences that rule on this finding
# palisade: ignore[PI-EXEC,PI-SQL]silences any of the listed rules
# palisade: ignoresilences every rule on this finding
- reason or : reason after the bracketsrecorded and reported

The comment goes on the sink line, or the line directly above it. # and // are both accepted, so the same syntax works in Python and JS/TS. Rule ids are case-insensitive.

Suppressions are deliberately loud, because a silent one is how a vulnerability quietly comes back:

  • suppressed findings are counted, not discarded, and reported in the terminal summary and in summary.suppressed_inline
  • each one appears in the suppressions array of --json with its rule, location, severity and reason
  • a comment that stops matching anything is reported as stale, so dead suppressions get cleaned up instead of masking a future finding

A suppression lives in the code being scanned, so anyone who can edit the code can silence a finding. That is the same trust model as # noqa, and the reason suppressions are counted and attributable rather than invisible. Review them in code review like any other change.

Configuration

pyproject.toml under [tool.palisade], or the same keys in .palisade.toml at the scan root (--config overrides discovery). Invalid config → warning + defaults, never a crash.

[tool.palisade]
paths_ignore = ["migrations/*", "sandbox/*"] # glob patterns, relative to root
include_tests = false # scan tests/** too
max_hops = 3 # inter-procedural depth bound
assume_params_untrusted = false # library mode default
rules_dir = "security/palisade-rules" # extra rules directory

CLI flags override config; an explicit assume_params_untrusted=False from an API caller overrides both.

JSON schema

scan --json emits one document. schema_version gates compatibility - parse defensively on any other value.

{
"schema_version": 1,
"tool": "palisade-sec 0.5.2",
"summary": {
"files_scanned": 6,
"high": 4, "med": 1, "low": 0,
"baseline_suppressed": 0, // known findings hidden by --baseline
"suppressed_inline": 0 // findings silenced by `palisade: ignore`
},
"suppressions": [ // each silenced finding, never hidden
{"rule": "PI-EXEC", "file": "app.py", "line": 42,
"severity": "high", "reason": "sandboxed", "suppressed_at": 42}
],
"findings": [ // sorted: severity, file, line, rule
{
"rule": "PI-EXEC",
"title": "Prompt injection reaching code execution",
"severity": "high", // high | med | low
"confidence": "HIGH", // HIGH | MEDIUM | LOW (path directness)
"risky_partial_defense": false, // true when downgraded (see below)
"file": "app.py", // sink location = finding location
"line": 64,
"fingerprint": "9f2c4a1b8e3d5f07", // baseline identity, line-independent
"count": 1, // duplicates collapsed into this entry
"trace": {
"source": { "file": "app.py", "line": 56, "snippet": "spec = request.json[\"spec\"]", "matched": "request.json" },
"llm": { "file": "app.py", "line": 57, "snippet": "resp = client.chat.completions.create(", "matched": "chat.completions.create" },
"sink": { "file": "app.py", "line": 64, "snippet": "exec(code)", "matched": "exec" }
},
"partial_defenses": [ // non-empty ⇒ severity was downgraded to med
{ "pattern": "is_blocked_code", "kind": "partial_defense", "file": "app.py", "line": 95 }
// kind: "partial_defense" (denylist/confirmation gate)
// | "unverified_sanitizer" (sanitizer in name only)
],
"attack": "...", "fix": "...", "references": ["CVE-...", "..."],
"cwe": ["CWE-94", "CWE-1426", "CWE-1427"],
"owasp_llm": ["LLM01:2025", "LLM05:2025"]
}
],
"skipped": ["broken.py: parse error, file skipped (...)"],
"warnings": ["invalid rule file skipped: bad.yaml: ..."],
"notes": ["1 JS/TS file(s) skipped - install ... palisade-sec[js] ..."]
}

Notes for consumers:

  • --json always includes all severities; the HIGH-first display filter applies to terminal output only.
  • In library mode, trace.source.matched is param:<name>; for route handlers it’s the matched source pattern (request.json, req.body, …).
  • notes may include an inter-procedural truncation notice on very deep call chains - recall, not precision, is what truncation affects.

Baseline file format

{
"schema_version": 1,
"tool": "palisade-sec 0.5.2",
"findings": {
"<fingerprint>": { "rule": "PI-EXEC", "file": "app.py", "severity": "high", "count": 1 }
}
}

Sorted by fingerprint; safe to merge in git. A missing or corrupt baseline degrades gracefully: a warning, and all findings treated as new.