How to Gracefully Stop or Interrupt a Running Strix Security Scan

To gracefully stop a Strix security scan, press Ctrl+C or Ctrl+Q in the TUI, send SIGINT/SIGTERM to the process, or programmatically set the _scan_stop_event flag to signal the background thread to exit cleanly.

Strix implements a robust cancellation mechanism that allows security scans to stop without corrupting data or leaving zombie threads. According to the usestrix/strix source code, the application manages scan lifecycle through a threading.Event object that coordinates between the UI, signal handlers, and the background scan thread. This architecture ensures resources are released properly regardless of whether the stop request comes from keyboard input, OS signals, or internal API calls.

Keyboard Shortcuts and Signal Handlers

The Strix TUI provides multiple entry points for requesting a shutdown. In strix/interface/tui.py, the StrixTUIApp class defines key bindings at lines 694–698 that map Ctrl+C to action_request_quit and Ctrl+Q to action_custom_quit. Both paths ultimately invoke the same graceful shutdown sequence.

For headless or scripted deployments, Strix registers custom signal handlers in _setup_cleanup_handlers (lines 61–76). These handlers capture SIGINT and SIGTERM, invoking self.tracer.cleanup() before terminating the process. Sending kill -2 <pid> from another terminal triggers the same cleanup path as pressing Ctrl+C.

The Stop Event Mechanism

At the core of the shutdown logic is a private _scan_stop_event (a threading.Event instance). When the scan starts in _start_scan_thread (lines 93–105), the code wraps the heavy agent.execute_scan work inside a nested scan_target function. Before entering the asyncio loop, the thread checks self._scan_stop_event.is_set(). If the flag is set, the scan is skipped entirely, allowing the thread to proceed directly to the finally block.

def _start_scan_thread(self) -> None:
    def scan_target() -> None:
        ...
        if not self._scan_stop_event.is_set():
            loop.run_until_complete(agent.execute_scan(self.scan_config))
        ...
        finally:
            loop.close()
            self._scan_completed.set()

The finally block guarantees that self._scan_completed.set() always runs, ensuring the UI can distinguish between a running scan and a finished one, even when interrupted.

Step-by-Step Graceful Shutdown

When a user initiates a stop, the following sequence executes:

  1. Trigger Detection – The TUI receives the keypress or the OS delivers SIGINT/SIGTERM to the process.

  2. action_custom_quit Execution – Located at lines 55–63 in strix/interface/tui.py, this method sets the stop event and waits for the worker thread:

    if self._scan_thread and self._scan_thread.is_alive():
        self._scan_stop_event.set()
        self._scan_thread.join(timeout=1.0)
    self.tracer.cleanup()
    self.exit()
  3. Thread Termination – The background thread detects the set event, skips execute_scan, closes its asyncio loop, and signals completion.

  4. Resource Cleanup – The tracer flushes telemetry data via tracer.cleanup(), and the TUI exits cleanly.

Implementation Examples

Interactive Terminal Stop

While the scan progress is visible in the terminal, press Ctrl+C or Ctrl+Q. The TUI will display the quit dialog, set the stop event, and return to the shell within one second.

Signal-Based Termination

For a headless Strix process running with PID 1234, send SIGINT from another shell:

kill -2 1234

The custom handler registered in _setup_cleanup_handlers will invoke the same cleanup sequence as the interactive method.

Programmatic Stop via Python API

Internal tooling can request a stop by directly manipulating the event flag:

from strix.interface.tui import StrixTUIApp

app = StrixTUIApp(args)

# ... after scan has started ...

app._scan_stop_event.set()

Direct manipulation of _scan_stop_event is intended for internal use; external callers should prefer UI shortcuts or OS signals to avoid thread-safety issues.

Summary

  • Strix uses a threading.Event (_scan_stop_event) as a cancellation token between the UI and the scan worker thread.
  • Ctrl+C, Ctrl+Q, SIGINT, and SIGTERM all trigger the same graceful shutdown path via action_custom_quit.
  • The scan thread checks the event before running execute_scan, ensuring it exits quickly without leaving orphaned asyncio loops.
  • A 1.0-second timeout on thread.join() prevents the UI from hanging if the thread is unresponsive, while tracer.cleanup() ensures telemetry data is flushed.

Frequently Asked Questions

What happens if I force-kill Strix with kill -9?

Sending SIGKILL (signal 9) terminates the process immediately without allowing the signal handlers in _setup_cleanup_handlers to run. This bypasses tracer.cleanup() and may result in incomplete telemetry logs or temporary file artifacts, though the scan itself will stop instantly.

Can I resume a scan after gracefully stopping it?

No. The _start_scan_thread method sets _scan_completed in its finally block, marking the scan lifecycle as finished. Strix does not persist intermediate scan state for resumption; you must restart the scan from the beginning.

How long does Strix wait for the scan thread to finish before forcing exit?

The action_custom_quit method calls self._scan_thread.join(timeout=1.0), giving the thread a maximum of 1.0 seconds to observe the stop event and shut down. If the thread exceeds this timeout, the TUI proceeds to self.exit(), though the thread will continue until Python process termination.

Is telemetry data saved when I interrupt a scan?

Yes. Both the keyboard quit path and the signal handler path invoke self.tracer.cleanup(), which persists any buffered telemetry data before the process exits. This ensures partial scan results are recorded even during an interruption.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →