Skip to content
Cisco AI Defense logo
CiscoAI Security

CLI parity — DefenseClaw

Overview

The DefenseClaw TUI never holds mutable state the CLI cannot reproduce. Every write is invoked as an argv-visible subprocess — palette entries, panel action menus, and forms all go through the same code path. This is deliberate: SOC operators working in the TUI and CI pipelines running the CLI produce byte-identical audit trails, and there is no way to perform an action in the TUI that would go undocumented on the CLI.

The subprocess bridge

Source: internal/tui/command.go (CommandExecutor).

Rendering diagram…

Key properties:

  1. Argv-visible — every action becomes a literal exec.Command(binary, args...). ps auxfww during a scan shows the same command line the operator would have typed.
  2. PTY-backed — subprocess output is streamed through a pty so progress spinners and TTY-aware prompts render correctly (github.com/creack/pty).
  3. CancellableCtrl+C in the TUI sends SIGINT to the subprocess only; the TUI itself keeps running.
  4. Sibling-resolved binaryresolveSiblingBin("defenseclaw") prefers a sibling of the running defenseclaw tui executable, falling back to PATH. A pipx install and a system package can coexist without the TUI accidentally invoking the wrong one.

What this buys you

ConcernResolution
Audit trailEvery action is logged at both the CLI layer (by defenseclaw) and the sidecar layer (by the REST call the CLI makes). The TUI adds nothing and hides nothing.
CI parityRunning the TUI's commands from CI produces the same rows in ~/.defenseclaw/audit.db and the same webhook notifications.
ReproducibilityBug reports can include the exact subprocess command line; operators rarely have to guess what the TUI "did under the hood".
SafetyA buggy TUI build cannot corrupt state in a way the CLI can't; worst case, it invokes the wrong subcommand.

How actions resolve arguments

When a palette entry has NeedsArg=true, the subprocess bridge fills the argument from (in order):

  1. The argument the palette prompted for.
  2. The currently-selected row in the panel that invoked the action.
  3. A panel-computed default (e.g. scan skill --all resolves to no argument because it is scoped --all).

The resolver is covered by command_resolve_test.go — adding a new CmdEntry without a corresponding resolver test case breaks CI.

Auditing TUI-originated actions

Three complementary audit trails exist:

LayerWhat it captures
TUI log~/.defenseclaw/tui.log — every subprocess invocation (binary, args, exit code, duration)
CLI auditThe CLI writes an entry to ~/.defenseclaw/audit.db for every verb (scan, block, set, etc.)
Sidecar audit~/.defenseclaw/audit.db also receives a row from the sidecar's REST call path

To confirm a suspected TUI action after the fact:

# Find recent subprocess invocations
tail -n 200 ~/.defenseclaw/tui.log | jq '{ts, cmd, args, exit_code}'

# Correlate with the audit DB
sqlite3 ~/.defenseclaw/audit.db \
  "select event_type, verdict, rule_id, timestamp
   from activity
   where timestamp > datetime('now','-1 hour')
   order by timestamp desc limit 20;"

correlation_id ties the two — the CLI emits it in its JSON output and the sidecar receives it as X-DefenseClaw-Correlation-Id on every REST call.

Deviations (intentional)

A handful of TUI behaviors are not 1:1 with the CLI. They are cosmetic, not functional:

  • Live refresh (5s / 30s intervals) is TUI-only; the CLI has status / audit stats but doesn't poll.
  • Filter bars are TUI UI — they produce filtered views by passing extra flags to the CLI (--severity, --type).
  • Fuzzy search in the palette has no CLI equivalent; it's a discovery aid.

None of these alter state or emit events on their own.

Parity tests

Parity is not a documentation promise — it's tested in CI:

  • cli_parity_test.go — every CmdEntry must have CLIBinary and CLIArgs set; the test executes a dry-run of each entry and asserts the CLI accepts the exact argv.
  • command_test.go — asserts there are no duplicate TUIName values; operators never see two palette entries for the same action.
  • command_resolve_test.go — asserts argument resolution for every NeedsArg=true entry.

Related