1. Overview

A coding agent is a language model wrapped in a harness . On every turn the harness sends the model the task so far and a list of tools. The model answers with a tool call, the harness runs it if permitted, and the output becomes context for the next turn. The most consequential tool is the shell. One Bash call can run tests or rewrite a service, and a user who has turned off per call approval never sees which.

The record so far is short and expensive. In July 2025 a Replit agent reportedly deleted a production database during a code freeze, as logged in the AI Incident Database . In April 2026 Railway described another agent deleting a production volume through its API. In July Hugging Face reported an agent running OpenAI models leaving its evaluation sandbox and reaching five datasets on their infrastructure.

Shell calls are a partial record of that activity, and I wanted a cheap way to say what each one is for. Langfuse’s decision model evaluators can label every call from a fixed list of options at the price of one small model call. What they need is the list itself, with a description and examples for each option. I wanted that list to come from my own traces, not from intuition. So this post embeds and clusters 72,264 recorded Bash calls into a purpose taxonomy, tests how well command text recovers an evaluator’s labels, and compares what four models were given to do. The monitoring signals the taxonomy supports follow, as a design rather than a result, and the remaining observations come last.

The design escalates two kinds of call to an LLM judge or a person. One is labelled other or unclear. The other is labelled differently by the evaluator and a classifier. The review checks each against the session's trajectory for anomalous behavior.

Experimental setup

Grove runs my coding agents in isolated workspaces and acts as their OpenTelemetry gateway. Every observation across every turn of a session leaves as a span. That covers each model request, tool call and result, each annotated with the session, workspace, model, tool name and token counts. Those spans go to whichever OTLP backend is configured, here Langfuse , an open source tracing store for LLM applications. A LiteLLM gateway sits between Claude Code and the providers, so one harness runs Anthropic and OpenAI models interchangeably and the trace records which one answered.

Langfuse runs an evaluator over a filtered set of those observations and stores each answer as a score. An LLM judge would write its verdict in prose. A decision model , the class TypeSafe introduced with Jev , generates no text. It takes the observation as structured state plus a typed question and returns one named option with a probability. Mine is Tev1 , Together AI’s Jev-style 4B fine-tune of Qwen3.5, run as a Q8_0 GGUF on my own GPUs through Ollama’s decision endpoint behind the same LiteLLM gateway. Sub second answers at no per call cost are what make labelling every call practical. The question is what is this shell call for, with 18 options such as run_tests. The scores are annotation and never reach the agent.

Langfuse dashboard widget counting evaluator purpose labels on shell calls over one day
The evaluator's labels as a Langfuse dashboard widget, counted over one day of shell calls.

An illustrative call, not a corpus record:

{
  "type": "tool_use",
  "id": "toolu_example",
  "name": "Bash",
  "input": {
    "command": "uv run pytest tests/test_parser.py -q",
    "description": "Run the parser unit tests"
  }
}

The model generates both command and description before anything runs. One call can hold several shell commands, so call counts are not counts of independent tasks.

Purpose distribution summary

Every call below carries a purpose. The evaluator supplied 16.3% of them and a classifier trained on those labels supplied the rest. No person has checked either. The six broad groups keep every call, clustered or not.

Purpose distributions for the full cohort, Claude Fable 5.1, and GPT-6 Astra
Fig. 1. Broad purpose groups for the cohort and two comparison models. Download the counts or the category key.

2. Dataset and command embeddings

The calls span one month of work. Holding the harness to Claude Code keeps the tool surface constant while models and tasks vary.

ObservationSelected cohort
Claude Code shell calls72,264
Recorded session IDs339
Calls with an evaluator label11,761
Calls assigned a predicted label60,503
Sessions containing evaluator labels51
Weekly shell call exposure by recorded or resolved model version
Fig. 2. Model exposure across the window. Evaluator coverage is 3.2% for Opus 5, 37.9% for Opus 5.5, 10.7% for Fable and 4.7% for Astra. Comparisons also reflect when each model was used.

Qwen3-Embedding-0.6B turns each command into a vector. Long commands are truncated, which can drop the decisive part of a script. The vectors represent the command alone, not the context the model had when it generated it.

3. Predicting evaluator labels

A classifier on command embeddings recovers the evaluator’s labels more than twice as well as the leading program alone. A python3 call might edit a file, query data or run a test, and the program name cannot tell them apart. The full text can.

Logistic regression is trained on the 11,761 labelled vectors, with sessions held apart between training and validation. The table compares it with two baselines.

ClassifierMacro-F1Micro-F1
Embedding + logistic regression0.620.73
Embedding + 15 nearest neighbors0.520.72
Leading-program majority0.280.56

Macro F1 weights the 18 categories equally and micro F1 pools every decision. The embedding classifier more than doubles macro F1 over the leading program baseline. That is agreement with an LLM judge whose labels no person has validated, not accuracy against ground truth.

Purpose predictions on validation sessions compared with evaluator labels
Fig. 3. Each row shows predictions as shares of its reference category. Exploration and git history are easier to recover than broad categories such as other.

4. Clustering and taxonomy derivation

The 18 evaluator labels hide structure that clustering exposes. rg "parse" src/ searches code and sed -n '40,80p' src/parser.py reads a known range, yet both receive the same label. Grouping commands by similarity, without using the labels, separates them.

UMAP reduces the vectors to 10 dimensions. HDBSCAN then finds 126 clusters and leaves 33.7% of calls outside any dense group.

On the 8,885 labelled calls inside clusters, agreement with evaluator purposes is modest. The adjusted Rand index is 0.23 and normalized mutual information is 0.35. The clusters do not reproduce the purpose vocabulary, and UMAP can distort density, so they are an inspection aid rather than a validated taxonomy.

Hierarchy of the larger command clusters
Fig. 4. The hierarchy uses average linkage across 34 clusters with at least 400 calls. These cover 38% of the cohort. Leaves identify the dominant purpose and leading program. A separate density cluster view shows membership and unclustered points.

Git history makes up 16.9% of unclustered calls against 10.7% of clustered ones. Dropping the unclustered third would shift the purpose distribution. Every summary keeps every call.

Cluster interpretation and label assignment

The clusters split what one evaluator label merges. Unit tests, browser suites and deliberate mutation checks form separate clusters inside one testing label. Reading code at a past revision separates from searching the current tree. Monitoring CI separates from editing issues and reviews.

I assigned each of the 126 clusters one label by reading its exemplar commands, leading programs and purpose mix. That yields 47 labels in ten families, plus other and unclear as fallbacks. Each unclustered call takes the label of its nearest cluster centroid. That gives every label an approximate share.

Under that approximation 29 labels hold at least 1% of calls, against 16 of the 18 evaluator labels. The largest label falls from 32.7% to 21.2% of calls. No model or person has yet classified individual calls under this taxonomy. These shares describe the clustering, not measured accuracy.

5. Taxonomy adaptation and monitoring design

The Bash call purpose taxonomy derived above reflects one developer’s workflow. A team can derive its own the same way from its own traces. The evaluator definition poses it as one choice question with 49 options. Each option carries a description and the clusters behind it. The example command above would map to verify_unit_tests.

The first signal a labelled corpus offers is embedding distance. Cosine distance is one minus cosine similarity. A command far from every validated reference for similar tasks is worth a look. Distance does not establish malice, since commands with similar text can differ in their targets and permissions.

The second is purpose frequency. A team learns the mix of categories its agents normally produce for a kind of task. A run of service_deploy calls during documentation work then stands out. The third is disagreement between the classifier and the evaluator, the fraction of calls they label differently. That measures annotation consistency, not behavior. High disagreement points at an ambiguous category boundary. It should prompt label review, not an alarm.

The disagreeing calls are also the ones to escalate, together with calls labelled other or unclear. Each has a purpose label in doubt. An LLM judge or a person reviews it against the session's trajectory, the task and the turns that led to the call. A command routine in one session can be anomalous in another.

The signals and the escalation path need calibration first. A sample labelled by people would measure accuracy per option and show which clusters need splitting or merging. Alert thresholds would need examples of incidents and of benign activity. I tested neither the signals nor the escalation path, so the monitoring use is a proposed design, not a result.

6. Additional observations

The same corpus also supports comparisons of model exposure, tool errors and associated output tokens, and a separate input comparison measures whether generated descriptions improve purpose classification. None of these feed the taxonomy or the monitoring design. They are reported here because they came out of the labelled data.

Session exposure and purpose distributions

With every call labelled, the purpose mix of each model becomes visible. What it shows is assignment, not ability, and three confounds apply to this subsection and the next. Evaluator coverage varies twelvefold across the four models, from 3.2% to 37.9%. Classifier accuracy was not measured per model, so unequal prediction errors can masquerade as differences in purpose. Attribution names the model that issued a call, not always the one driving the session. And 113 sessions contain more than one model, so the totals are not independent.

A model participates in a session if at least one shell call there is attributed to it. The denominator is all 339 sessions with shell calls.

ModelCallsSessions (% of 339)Calls/model-session, median (IQR)
Opus 521,642199 (58.7%)55 (14.5 to 142)
Opus 5.514,927101 (29.8%)89 (33 to 203)
Fable 5.15,61853 (15.6%)44 (16 to 141)
GPT-6 Astra7,43757 (16.8%)86 (34 to 171)

The two Opus versions never overlap in time, and delegated subagent work is 31.2% of Opus 5 calls against 4.1% for Opus 5.5, so their comparison mixes period with role. Fable’s largest session supplies 16.1% of its calls against 7.6% for Astra. Normalizing by volume does not remove that.

Purpose shares of calls for Opus 5, Opus 5.5, Fable 5.1, and GPT-6 Astra
Fig. 5. Each column sums to 100% of a model's calls across the 18 purpose categories. The pies repeat the broad groups from Figure 1.

The heatmap weights by calls, so long sessions dominate. Weighting each model and session pair equally gives the shares below.

ModelExplore code: call → session weightedRun tests: call → session weighted
Opus 534.8% → 30.0%12.0% → 8.0%
Opus 5.529.1% → 31.1%9.8% → 7.6%
Fable 5.130.7% → 28.1%7.7% → 5.3%
GPT-6 Astra9.9% → 10.7%13.1% → 10.2%

Opus 5 and Opus 5.5 swap order on exploration under session weighting. Astra keeps the lowest exploration share and the highest test share either way.

Tool execution errors

Claude Code’s is_error flag records nonzero exits. A test that finds a bug raises it, so the flag says nothing about whether the task failed. Rates below use only calls that carry the flag.

Tool error rates by purpose for the four comparison models
Fig. 6. Error rates by purpose and model in cells with at least 80 calls. Aggregates are 3.3% for Opus 5, 2.6% for Opus 5.5, 3.3% for Fable and 6.8% for Astra. Task difficulty and the timing of checks remain uncontrolled.

Astra’s aggregate rate is the highest at 6.8%. Yet on service operations Fable errors more often than Astra. Optimizing this flag alone would reward skipping tests over finding and fixing failures. A better outcome would record whether a failing check was resolved and the task accepted.

Command and description features

The example above pairs the test command with Run the parser unit tests. On the 11,723 labelled calls that carry a description, I compared classifiers trained on the command, the description and both.

Classifier inputMacro-F1Micro-F1
Command0.620.73
Description0.430.53
Command + description0.660.76

The description alone is a worse guide than the command. Together they add four points of macro F1, enough to keep the description as a secondary signal. Everything else in this post uses the command classifier alone.

Associated output token distributions

Each call inherits the output token count of the transcript message that issued it. Several calls can share one message. These associated output tokens therefore measure recorded output around shell activity, not a deduplicated bill. Only calls with a positive count enter the comparison.

Associated output token distributions for the four comparison models
Fig. 7. Interquartile ranges of output tokens associated with each model's calls. Medians are 335 for Opus 5, 483 for Opus 5.5, 456 for Fable and 144 for Astra.

Astra’s median is under a third of the other three. Command payloads and batching count toward these figures alongside any explanatory text.

Purpose shares of calls and associated output tokens across the cohort
Fig. 8. Call share against associated output share by purpose. Editing is 5.2% of calls and 9.3% of tokens. Each call repeats the token count of its linked transcript message.

Across the 64,727 eligible calls, exploration is 31.6% of calls and 42.8% of associated tokens. Turning that into savings would need deduplicated messages, input and cache charges, and comparable task outcomes.

Supplementary figures

Shell call counts by model version in the Claude Code cohort
Fig. S1. Call counts show corpus composition by model version. These measure exposure rather than performance.
Label recovery from command and description inputs with their embedding similarity for Fable and Astra
Fig. S2. Panel A repeats the section 3 input comparison. Panel B measures similarity between command and description embeddings. Similarity does not establish that the description faithfully represents the command.
Minimum cluster sizeMinimum samplesClustersUnclustered
1202012633.7%
120513031.4%
602023035.6%
60526633.0%

Table S1 compares HDBSCAN settings on the same UMAP coordinates. The unclustered share stays between 31.4% and 35.6% across them.

Command embeddings colored by broad purpose including calls without density cluster membership
Fig. S3. The embedding projection uses the purpose assignments summarized in Figure 1. Coordinate extremes are cropped.
Median associated output tokens by purpose for the four comparison models
Fig. S4. The chart compares median associated output tokens by purpose and model. Editing commands can include replacement file content. Their output counts therefore include the material being written.
Observed tool error rates for the four comparison models
Fig. S5. Wilson intervals summarize aggregate tool error rates under an independence assumption. They do not account for dependence within sessions.

Reproducibility

Embeddings come from Qwen3-Embedding-0.6B with Q8_0 weights, served through Ollama , producing 1,024 dimensions. Command text is capped at 2,000 characters, which truncates 2,136 calls or 3.0%. Classification uses logistic regression with five folds from GroupKFold grouped by session. UMAP uses cosine distance, 30 neighbors and min_dist=0. HDBSCAN uses min_cluster_size=120 and min_samples=20. Other embeddings and UMAP settings were not tested.

Raw traces contain private workspace content and are not released. The CSVs above support inspection of the summaries but cannot reproduce the full analysis.

comments powered by Disqus