# How Worktrunk Ensures Consistent Subprocess Logging and Explicit Untracing

> Learn how Worktrunk ensures consistent subprocess logging and explicit untracing by wrapping commands in CommandTrace, guaranteeing all foreground subprocesses are recorded in trace.jsonl.

- Repository: [Maximilian Roos/worktrunk](https://github.com/max-sixty/worktrunk)
- Tags: internals
- Published: 2026-09-14

---

**Worktrunk enforces consistent subprocess logging by wrapping every external command in a `Cmd` wrapper that instantiates a `CommandTrace` object, which panics in its `Drop` implementation unless explicitly resolved via `complete()` or `fail()`, ensuring every foreground subprocess is recorded in `trace.jsonl` while background processes are deliberately excluded.**

The `max-sixty/worktrunk` repository implements a rigorous tracing system that eliminates silent subprocess execution. By leveraging Rust's ownership model and the **must-resolve pattern**, Worktrunk guarantees that every foreground command appears in the unified trace log, while making untraced background jobs an explicit, documented design choice.

## The CommandTrace Must-Resolve Pattern

At the core of Worktrunk's consistent subprocess logging is the `CommandTrace` type defined in [`src/trace/emit.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/trace/emit.rs). This structure enforces a strict contract where every trace instance must be explicitly resolved before the object is dropped, preventing any command from executing without generating a trace record.

### Automatic Instantiation in the Cmd Wrapper

Every external command execution begins with the `Cmd` wrapper in [`src/shell_exec.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/shell_exec.rs). At **line 1570**, the code instantiates `CommandTrace::new(context, cmd_str)`, capturing the command string and operational context immediately upon construction.

```rust
// Simplified representation of the pattern in src/shell_exec.rs
let mut trace = CommandTrace::new(Some("worktree"), "git status");
let output = Cmd::new("git")
    .args(["status", "--porcelain"])
    .run()?;

```

### Explicit Resolution Paths

The `CommandTrace` type requires developers to choose between two terminal states. After a successful execution, the code invokes **`CommandTrace::complete(dur_us, true)`** at **line 1711** of [`src/shell_exec.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/shell_exec.rs), writing a completed record with microsecond duration to the trace log. When execution fails, **`CommandTrace::fail(dur_us, err)`** at **line 1727** records the error state and failure details.

```rust
// Resolution after successful execution
trace.complete(1200, true)?;  // duration: 1200µs, success: true

// Resolution after error
trace.complete(500, false)?;  // Or using fail() for explicit error handling

```

### Panic-on-Drop Enforcement

The enforcement mechanism resides in **[`src/trace/emit.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/trace/emit.rs) at lines 352-360**. The `Drop` implementation asserts that the trace has been resolved; if neither `complete()` nor `fail()` was called before the object goes out of scope, the program panics with a clear error message. This compile-time and runtime guarantee ensures no foreground subprocess can silently bypass logging.

## Distinguishing Traced Foreground from Untraced Background Processes

Worktrunk differentiates between foreground commands that must be logged and background processes that should remain untraced. This distinction is implemented through deliberate structural choices in the codebase.

### Foreground Subprocess Coverage

Every higher-level component that spawns external tools creates a `CommandTrace` before invoking `Cmd`. Examples include:

- **Git plumbing commands** at [`src/git/repository/mod.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/git/repository/mod.rs) line 1795, where repository operations are fully traced.
- **Concurrent pipelines** at [`src/output/concurrent.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/output/concurrent.rs) line 283, ensuring parallel child processes each generate distinct trace entries.
- **Custom user commands** at [`src/commands/custom.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/custom.rs) line 146, applying the same logging discipline to arbitrary external tools.

### Deliberate Exclusion of Background Jobs

Detached child processes, such as background jobs launched via [`src/commands/process.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/process.rs), are intentionally **not** wrapped in `Cmd`. These processes bypass `CommandTrace` creation entirely, excluding them from `trace.jsonl` as documented in the surrounding code comments. This design accommodates long-running daemons that outlive the parent Worktrunk process or interactive helper applications where tracing provides no value.

```rust
use std::process::Command;

// Example: a background job that is intentionally untraced
// This spawns a detached child without CommandTrace
Command::new("some-long-running-daemon")
    .arg("--quiet")
    .spawn()?;  // Intentionally omitted from trace.jsonl

```

## Practical Implementation Examples

The following examples illustrate how Worktrunk implements the tracing contract internally and how background processes avoid it:

```rust
use worktrunk::shell_exec::Cmd;
use worktrunk::trace::CommandTrace;

// Foreground command with mandatory trace resolution
let mut trace = CommandTrace::new(Some("worktree"), "git status");
let output = Cmd::new("git")
    .args(["status", "--porcelain"])
    .current_dir("/path/to/worktree")
    .run()?;
    
// Explicit success logging is required to avoid panic on drop
trace.complete(1200, true)?;

```

```rust
use std::process::Command;

// Background daemon that is explicitly untraced
// No CommandTrace is created, so this process is omitted from trace.jsonl
Command::new("persistent-worker")
    .arg("--detach")
    .spawn()?;

```

## Summary

- **`CommandTrace` is a must-resolve type** that panics on drop if not completed, ensuring no subprocess executes without a trace entry.
- **Foreground subprocesses are uniformly logged** through the `Cmd` wrapper, with coverage extending across Git operations, concurrent pipelines, and custom commands.
- **Background processes are explicitly excluded** by bypassing the `Cmd` wrapper in [`src/commands/process.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/process.rs), making untraced execution a deliberate architectural choice.
- **Trace data aggregates to `trace.jsonl`**, providing a unified log for debugging, profiling, and reproducibility across all traced operations.

## Frequently Asked Questions

### What happens if a developer forgets to resolve a CommandTrace?

The application will panic when the `CommandTrace` object is dropped. According to the implementation in [`src/trace/emit.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/trace/emit.rs) at lines 352-360, the `Drop` implementation asserts that either `complete()` or `fail()` was called, preventing any silent omissions from the trace log.

### Can background processes be traced if needed?

Technically yes, but it requires wrapping them in the `Cmd` structure instead of using raw `std::process::Command`. However, the codebase intentionally avoids this for detached children in [`src/commands/process.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/commands/process.rs) because background jobs often outlive the parent process, making trace correlation difficult and potentially misleading.

### Where does Worktrunk store subprocess trace data?

All resolved `CommandTrace` records are written to a `trace.jsonl` file in JSON Lines format. This centralized log aggregates timing, exit status, and command metadata from every traced foreground subprocess across the entire Worktrunk session.

### How does CommandTrace handle concurrent subprocess execution?

Each concurrent pipeline child receives its own `CommandTrace` instance. As shown in [`src/output/concurrent.rs`](https://github.com/max-sixty/worktrunk/blob/main/src/output/concurrent.rs) at line 283, parallel subprocesses are traced individually, with each maintaining its own resolution contract to ensure complete logging even during concurrent execution.