Incomplete Recipient Validation in Claude Code Computer Use Enabled Command Execution in an Unapproved Terminal

HackerOne #3985968

High · Triaged

Amartya Jha

In this Security Research

No headings found on page
One hold_key action is approved while Grapher is frontmost. Its first Return completes Grapher's quit, macOS restores focus to unapproved iTerm, and later components type and submit a harmless shell command.

TL;DR

  • Claude Code Computer Use limits desktop control through per-application approval. Approving an ordinary application does not approve a terminal, which Anthropic flags as equivalent to shell access.

  • An earlier CodeAnt finding (fixed in Claude Code 2.1.243) showed a multi-event type action could continue into an unapproved terminal after focus changed. Anthropic fixed type and repeated key by rechecking the recipient inside those loops.

  • The same invariant was not applied to hold_key. In Claude Code 2.1.252, hold_key checked the frontmost application once, then handed the full key sequence to a native macOS executor that pressed each component without rechecking the recipient.

  • We proved it: one composite action began on Grapher's Save-and-Quit sheet, its first Return quit Grapher, macOS returned focus to unapproved iTerm, and later components typed touch z and pressed Return. The shell executed it. Three exploit runs created the marker; three matched controls did not; a direct iTerm invocation was blocked.

  • Anthropic confirmed the root cause through code review, rated it High (CVSS 4.0 score 7.7), and awarded a bounty. Remediation is in progress and the report is still private.

At a Glance

Component

Claude Code Computer Use (macOS)

Vulnerability

Incomplete-fix bypass of per-application recipient validation in the hold_key path

Related fix

Recipient-continuity fix in Claude Code 2.1.243 (report #3930758)

Impact

One approved Grapher action continued into unapproved iTerm and executed a shell command

Root cause

hold_key checks the frontmost app once, then delegates the full component sequence to a native executor that has no recipient identity

Class

Authorization / TOCTOU (checked-versus-consumed recipient mismatch)

Severity

High, CVSS 4.0 score 7.7

Affected

Claude Code 2.1.252 (macOS arm64)

CVE

None assigned (report private, remediation in progress)

Report

HackerOne #3985968

Found by

CodeAnt AI Security Research Team

Claude Code Computer Use is powerful because it generates trusted native input on the user's real desktop. Its central security control is a per-application approval list: a user can approve an ordinary application, such as Apple's Grapher, without approving a terminal, which the product identifies as equivalent to shell access.

An earlier vulnerability the CodeAnt AI Security Research Team reported showed that Claude Code could authorize a multi-event type operation while Grapher was frontmost, then continue sending its remaining keystrokes after Grapher quit and unapproved iTerm became the recipient. Anthropic fixed that path in Claude Code 2.1.243 by checking the frontmost application during the input loop. In this follow-on finding, we show that the same security invariant was not applied to the separate hold_key action, so the protected terminal boundary could still be crossed.

The Application Approval Boundary

Computer Use is powerful because it generates trusted native input on the user's real desktop. Its principal security boundary is therefore not merely whether a tool call was approved. It is which application is allowed to receive every event produced by that call.

Anthropic's Computer Use documentation states that Claude can control only applications approved for the current session, and it gives terminal and IDE applications an "Equivalent to shell access" warning. Those statements establish a clear distinction between approving Grapher and approving iTerm.

The relevant invariant is:

for every native input event:
  current recipient == approved application
for every native input event:
  current recipient == approved application
for every native input event:
  current recipient == approved application

In our proof, the complete grant state remained equivalent to:

{
  "allowedApps": [
    { "bundleId": "com.apple.grapher", "displayName": "Grapher", "tier": "full" }
  ],
  "clipboardRead": false,
  "clipboardWrite": false,
  "systemKeyCombos": false
}
{
  "allowedApps": [
    { "bundleId": "com.apple.grapher", "displayName": "Grapher", "tier": "full" }
  ],
  "clipboardRead": false,
  "clipboardWrite": false,
  "systemKeyCombos": false
}
{
  "allowedApps": [
    { "bundleId": "com.apple.grapher", "displayName": "Grapher", "tier": "full" }
  ],
  "clipboardRead": false,
  "clipboardWrite": false,
  "systemKeyCombos": false
}

iTerm's bundle identifier, com.googlecode.iterm2, never appeared in the grant list.

Why This Was an Incomplete Fix

The earlier report and this finding reach the same protected terminal boundary, but they use different implementation paths.

The original issue affected type. Its implementation emitted a series of graphemes, Return events, and Tab events after one initial application check. Claude Code 2.1.243 introduced a recipient-continuity helper and called it inside the type loop. The repeated key path also checked recipient identity inside its repeat loop. The hold_key path received only a check before its native component loop:

Action

Where Recipient Identity Was Checked

Result After Focus Changed

type

Before each emitted grapheme or control key

Remaining input stopped

repeated key

Before each repeated key event

Remaining input stopped

hold_key

Once before the complete sequence entered the executor

Later components continued

This distinction matters. We did not claim that Anthropic's type fix failed. The fixed type path remained effective. The bypass existed because a sibling multi-event action implemented the same security requirement in a different loop.

Claude Code 2.1.243 moved recipient checks inside the type and repeated-key loops. The hold_key path performed one pre-delivery check, then delegated the complete component sequence to a native executor with no recipient identity.

Root Cause: Policy Stopped at the Executor Boundary

Reverse engineering the signed Claude Code 2.1.252 runtime, we found that the core Computer Use layer captured the authorized application and performed one focus-continuity check before calling the native executor. Reduced to the security-relevant control flow, the affected path was equivalent to:

const authorization = await authorizeKeyboardAction(context);
if (authorization.blocked) return authorization.result;

const approvedApp = authorization.approvedFrontmost;
const components = parseHoldSequence(input);

const changed = await checkFrontmostApp(approvedApp);
if (changed) return abortBeforeDelivery();

await executor.holdKey(components, duration, isAborted);
return success("Key held.");
const authorization = await authorizeKeyboardAction(context);
if (authorization.blocked) return authorization.result;

const approvedApp = authorization.approvedFrontmost;
const components = parseHoldSequence(input);

const changed = await checkFrontmostApp(approvedApp);
if (changed) return abortBeforeDelivery();

await executor.holdKey(components, duration, isAborted);
return success("Key held.");
const authorization = await authorizeKeyboardAction(context);
if (authorization.blocked) return authorization.result;

const approvedApp = authorization.approvedFrontmost;
const components = parseHoldSequence(input);

const changed = await checkFrontmostApp(approvedApp);
if (changed) return abortBeforeDelivery();

await executor.holdKey(components, duration, isAborted);
return success("Key held.");

The native macOS executor received the parsed components, the duration, and an abort callback. It did not receive the approved application identity. Its loop was equivalent to:

for (const component of components) {
  if (aborted) return;
  await keyboard.press(component);
  pressed.push(component);
}
for (const component of components) {
  if (aborted) return;
  await keyboard.press(component);
  pressed.push(component);
}
for (const component of components) {
  if (aborted) return;
  await keyboard.press(component);
  pressed.push(component);
}

The loop knew whether the operation had been globally aborted, but it could not determine whether Grapher still owned focus. Once delivery began, the authorization layer had no opportunity to stop a later component from reaching a different process. This created a precise checked-versus-consumed mismatch:

checked recipient:
  Grapher, immediately before native delivery

consumed recipients:
  Grapher for the first component
  iTerm for later components after Grapher exits
checked recipient:
  Grapher, immediately before native delivery

consumed recipients:
  Grapher for the first component
  iTerm for later components after Grapher exits
checked recipient:
  Grapher, immediately before native delivery

consumed recipients:
  Grapher for the first component
  iTerm for later components after Grapher exits

The first key component caused the identity transition itself. No separate focus-stealing helper, malicious approved application, or attacker-controlled scheduling race was required.

Controlled Reproduction

Our final reproducer used the official Anthropic-signed Claude Code 2.1.252 binary on macOS arm64 and Apple's unmodified Grapher application.

Item

Verified Value

Product

Claude Code

Platform

macOS arm64

Affected version

2.1.252

Binary SHA-256

b661c6a094fcc32656bf7c0071c5b45bf900b34d4f0a1ab3d78fd59aeba2c2c7

Approved application

Grapher only

Protected sink

Unapproved iTerm2 shell

Composite components

265

Encoded input size

1,311 bytes

A localhost Messages fixture supplied deterministic Computer Use calls. It did not invoke a production model, retain an authorization header, patch Claude Code, or replace the Computer Use policy and executor. The proof used the stock signed binary and native macOS input delivery.

Direct iTerm Negative Control

We first brought iTerm to the foreground and invoked the exact composite hold_key sequence. Claude Code rejected the operation:

"iTerm2" is not in the allowed applications and is currently in front
"iTerm2" is not in the allowed applications and is currently in front
"iTerm2" is not in the allowed applications and is currently in front

No marker was created. This established that iTerm was protected, the application gate was active, and the grant list had not accidentally included the terminal.

Transition Path

The candidate path created a new Grapher document and opened its standard Save-and-Quit sheet. iTerm remained behind Grapher with a blank prompt in a dedicated temporary directory. The single hold_key sequence had this shape:

Return
+ 256 Left components
+ t + o + u + c + h + Space + z
+ Return
Return
+ 256 Left components
+ t + o + u + c + h + Space + z
+ Return
Return
+ 256 Left components
+ t + o + u + c + h + Space + z
+ Return

The first Return confirmed Grapher's save sheet and completed the quit. The repeated Left components kept the same tool call active long enough for the stock application lifecycle to complete. They did not invoke a shortcut or special macOS behavior. After focus returned to iTerm, the later components typed the harmless command and the final Return submitted it.

The terminal was empty before the operation:

before: terminal-target %
after:  terminal-target % touch z
        terminal-target

before: terminal-target %
after:  terminal-target % touch z
        terminal-target

before: terminal-target %
after:  terminal-target % touch z
        terminal-target

The created empty file proved that the shell processed the command. It was not merely rendered text.

Matched No-Transition Control

The matched control used the same Claude Code binary, the same Grapher-only approval, the same iTerm target, the same 265-component sequence, and the same Computer Use implementation. Only the application lifecycle changed. Grapher remained alive and frontmost. The input stayed in Grapher and the iTerm marker remained absent.

The direct iTerm invocation was denied. Three Grapher-to-iTerm transition runs created the marker, while three matched controls that kept Grapher frontmost did not.

Reproduction Matrix

Our standalone reproducer completed three exploit runs and three matched controls. Every assertion passed.

Case

App at Authorization

App at End

iTerm Approved

Command Pre-Staged

Marker

Direct iTerm negative

iTerm

iTerm

No

No

Absent; blocked

Exploit run 1

Grapher

iTerm

No

No

Created

Exploit run 2

Grapher

iTerm

No

No

Created

Exploit run 3

Grapher

iTerm

No

No

Created

Control run 1

Grapher

Grapher

No

No

Absent

Control run 2

Grapher

Grapher

No

No

Absent

Control run 3

Grapher

Grapher

No

No

Absent

The exact sequence produced the following stable properties in all exploit runs:

  • the call began with com.apple.grapher frontmost;

  • Grapher's save sheet was present;

  • the operation returned Key held.;

  • com.googlecode.iterm2 was frontmost after delivery;

  • Grapher remained the only approved application;

  • all optional grant flags remained false; and

  • the empty z marker existed in the terminal target directory.

Capability Delta and Impact

The security impact is best expressed as a capability transition:

before:
  model-controlled input cannot type into iTerm
  direct hold_key invocation is denied

after beginning in approved Grapher:
  the same model-controlled call delivers later events to iTerm
  the shell accepts and executes the submitted command
before:
  model-controlled input cannot type into iTerm
  direct hold_key invocation is denied

after beginning in approved Grapher:
  the same model-controlled call delivers later events to iTerm
  the shell accepts and executes the submitted command
before:
  model-controlled input cannot type into iTerm
  direct hold_key invocation is denied

after beginning in approved Grapher:
  the same model-controlled call delivers later events to iTerm
  the shell accepts and executes the submitted command

Approval for Grapher does not otherwise grant command execution. Reaching iTerm bypassed the separate decision that carries the product's shell-access warning.

A prompt-injected or otherwise model-controlled Computer Use sequence could use this primitive to enter commands under the user's normal host identity. Depending on the command and local environment, that could expose user-readable information, modify source or configuration, start processes, install software, or make network requests.

This was not unconditional remote code execution. Computer Use had to be enabled, Grapher had to be approved for the session, an unapproved terminal had to receive focus after Grapher exited, and attacker-influenced model input had to choose the sequence. These conditions are reflected in the AT:P and UI:P metrics. They do not remove the bypass of the explicit per-application approval boundary.

Anthropic assigned the following CVSS 4.0 vector:




text


CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N (7.7)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N (7.7)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N (7.7)

Vendor Validation

Anthropic confirmed the underlying issue through code review. Its triage response identified the same root cause: hold_key validates the approved frontmost application before delivery begins but does not revalidate between the components of a composite sequence. Anthropic classified the report as High and later awarded a bounty for a High-severity finding on a core asset.

At the editorial cutoff, Anthropic reported that remediation was still in progress. The HackerOne report remained triaged and private. No fixed version, public advisory, or CVE identifier had been supplied.

Remediation Principles

The immediate correction is to extend recipient continuity through the entire native delivery loop.

  1. Pass the approved recipient into the executor. The native layer needs the bundle identifier and, preferably, process and window identity captured during authorization.

  2. Revalidate before every component. Each key press must confirm that the intended application remains the actual recipient.

  3. Abort and release safely. If identity changes, stop delivery and release any components already held down before returning an error.

  4. Bind to a stronger object than a display name. Bundle identifier alone may not distinguish process replacement or window transitions. Process identity and accessibility target should be retained where possible.

  5. Bound composite inputs. Component and encoded-size limits reduce abuse and simplify review, but they are defense in depth, not a substitute for recipient checks.

  6. Test every multi-event sibling. type, repeated key, hold_key, batches, paste paths, drag sequences, and any other loop that emits multiple native events require the same invariant.

[Figure 4: fig-4.svg] A robust design carries the authorized application identity into the native executor, checks it before every component, and aborts with safe key release as soon as the recipient changes.

Lessons for Security Engineering

A Security Helper Is Effective Only Where It Is Called

Claude Code 2.1.243 contained the correct recipient-continuity primitive. The remaining weakness came from placement. Calling the helper once before a second loop did not protect the events produced inside that loop.

Fixes Should Be Reviewed by Invariant, Not Only by Function

The original defect was not fundamentally a type bug. It was a violation of the rule that every event must be delivered only to an approved recipient. Reviewing all implementations of that invariant would have included hold_key and the native executor.

Native Executors Are Part of the Authorization Path

The high-level policy layer authorized Grapher correctly. The failure occurred after delegation, where the native layer had insufficient context to preserve that decision. Security metadata must travel with the operation through every layer that can change its meaning.

Deterministic Lifecycle Transitions Produce Stronger Evidence

The proof did not depend on racing a user or external process to steal focus. The authorized operation itself completed a stock application's quit sequence. That made the identity transition repeatable and isolated the missing inner-loop check.

Negative Controls Must Demonstrate the Defended Capability

The direct iTerm denial showed that shell input required approval. The no-transition control showed that the sequence remained harmless while Grapher stayed frontmost. Together, they proved that the application transition, not the payload alone, unlocked the protected sink.

Are You Affected?

The vulnerable behaviour is in Claude Code 2.1.252 on macOS with Computer Use enabled. You are in scope of the pattern if you use Computer Use, approve a non-terminal application for a session while a terminal (iTerm, Terminal, or an IDE terminal) is open in the background, and run attacker-influenced or untrusted content that can steer the Computer Use sequence.

Because remediation is still in progress at the time of writing, the practical guidance is to be deliberate about enabling Computer Use around untrusted content, avoid keeping a terminal focus-reachable behind an approved application during agent-driven sessions, and update Claude Code once Anthropic confirms a fix, then re-verify the fixed version.

Disclosure Timeline


Date

Event

1 September 2026

Report #3985968 submitted to Anthropic through HackerOne

3 September 2026

Anthropic confirmed the root cause through code review and triaged the report High at 7.7

14 September 2026

Anthropic stated that remediation remained in progress and no fix had been released

16 September 2026

Anthropic awarded a bounty for the High-severity finding

21 September 2026

Editorial cutoff; report remained private and triaged, with no fixed version supplied

Conclusion

Claude Code's original recipient-continuity fix corrected type, but the broader invariant stopped at a sibling executor boundary. In the affected hold_key path, authorization described Grapher only at the start of the call. The native component loop then continued after Grapher exited and delivered trusted events to unapproved iTerm.

The durable rule is simple:

Authorization must be checked against the application receiving each native event, including events emitted by composite actions and delegated executor loops.

This case demonstrates why incomplete-fix research should follow security invariants across adjacent implementations. A patch can be correct in the function it changes while the same boundary remains open in a sibling path.

References:

This research was conducted under Anthropic's vulnerability disclosure program by the CodeAnt AI Security Research team. Testing used stock local applications, a loopback fixture, and a harmless temporary marker. No third-party data or production service was accessed.

Building an agentic tool that generates native input or drives a desktop, and want its authorization boundary reviewed? Reach us at securityresearch@codeant.ai

BOOK A DEMO

[FAQ]

Frequently Asked
Questions

What is the Claude Code Computer Use vulnerability?

Is Claude Code Computer Use safe to use?

What versions of Claude Code are affected?

What is an incomplete fix or patch bypass?

What is a TOCTOU vulnerability, and how does it apply here?

Has a CVE been assigned?

Why is this rated High (7.7) rather than Critical?

[GET STARTED]

Find out what's already

exploitable in your codebase.

Find out what's already

exploitable in your codebase.

Find out what's already exploitable in your codebase.

START PENTEST

NO CC REQUIRED