Stale Application Authorization in Claude Code Computer Use Enabled Cross-Application Keystroke Delivery on macOS

CVSS 4.0

Amartya Jha

In this Security Research

No headings found on page

TL;DR

  • Claude Code Computer Use gates desktop control by per-application approval. A terminal such as iTerm carries a shell-access warning and must be approved separately from an ordinary app.

  • In Claude Code 2.1.226, a type operation checked the frontmost approved application once, before it started, then emitted every grapheme, Return, and Tab without rechecking the recipient.

  • Researchers at CodeAnt AI used one type call that began on Grapher's Save-and-Quit sheet: the first Return saved and quit Grapher, macOS restored focus to an unapproved iTerm window, and the remaining keystrokes and final Return executed a harmless shell command. Only Grapher was ever approved.

  • No focus-stealing race and no malicious application were required. A stock application's normal quit lifecycle caused the transition. This is a stale-authorization (checked-versus-consumed, TOCTOU) bug.

  • Anthropic rated it High (CVSS 4.0 score 7.7), fixed it in 2.1.243 so input now stops when the approved application loses focus, and resolved the report after a three-run retest.

At a Glance

Component

Claude Code Computer Use (macOS)

Vulnerability

Stale application authorization in the type operation (cross-application keystroke delivery)

Class

Authorization boundary bypass, checked-versus-consumed (TOCTOU) recipient mismatch

Chain

One approved Grapher type call continues into unapproved iTerm and runs a shell command

Root cause

type checks the frontmost app once, then emits every event without rechecking the recipient

Affected

Claude Code 2.1.226 (macOS arm64)

Fixed in

Claude Code 2.1.243 (verified by retest)

Severity

High, CVSS 4.0 score 7.7

CVE

None assigned (report private at editorial cutoff)

Report

HackerOne #3930758

Status

Validated, fixed, and resolved after retest; private at editorial cutoff

Found by

CodeAnt AI Security Research Team

Claude Code Computer Use is powerful because it produces trusted native input on the user's real macOS desktop. Its central control is per-application approval: the user approves specific applications for the session, and terminals receive a particularly strong warning because controlling one is equivalent to shell access.

Researchers at CodeAnt AI found that in Claude Code 2.1.226, that approval had a lifetime longer than the approved application itself. A single type operation checked the recipient once, at the start, and then kept delivering keystrokes after focus moved to an application the user never approved. Here is the whole finding.

The Security Boundary

Computer Use approval is not a grant to control the desktop as a whole. It is a grant to control specific applications for the current session. Anthropic's Computer Use documentation distinguishes applications by risk and describes terminal or IDE approval as equivalent to shell access.

That design implies an event-recipient invariant:

for every trusted input event:
  the application receiving the event
  must still be an approved application
  at the moment of delivery
for every trusted input event:
  the application receiving the event
  must still be an approved application
  at the moment of delivery
for every trusted input event:
  the application receiving the event
  must still be an approved application
  at the moment of delivery

The relevant authorization object is not only the tool call. It is the application, process, window, and accessibility target that receives each event produced by that call.

In the controlled session, the entire grant state was 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, was absent before and after the operation.

Why Grapher Produced a Deterministic Transition

Early experiments with ordinary focus changes were unsuitable because they depended on scheduling and could be dismissed as unreliable desktop behavior. The final proof instead used a stock application lifecycle that the input operation itself completed.

The sequence was:

  1. Open a fresh document in Apple's Grapher.

  2. Select Grapher > Quit so the standard Save-and-Quit sheet appears.

  3. Keep a dedicated iTerm window behind Grapher.

  4. Start one Computer Use type call while Grapher and its save sheet are frontmost.

  5. Use the first Return in that call to confirm the save and complete Grapher's quit.

  6. Observe macOS transfer focus to the existing iTerm window.

  7. Observe the same type call continue emitting its remaining characters.

The call did not pause for a second Claude Code authorization decision when the recipient changed. It retained the decision made for Grapher and delivered the rest of the input to iTerm.

The proof used a long, controlled payload and a harmless marker command. The marker established that the final Return was not merely displayed in a terminal: the shell actually processed the command.

Root Cause: One Check Before a Multi-Event Loop

Reverse engineering the signed 2.1.226 runtime, we isolated two functions in the extracted JavaScript. The minified names are included to make the analysis reproducible, but the excerpts below are reduced to the security-relevant control flow.

The Application Check

The function identified as fet obtained the frontmost application and looked up its bundle identifier in the session grant list. If the application was not approved, it returned app_not_granted:

let frontmost = await executor.getFrontmostApp();
let grants = new Map(allowedApps.map(app => [app.bundleId, app.tier]));

let tier = frontmost ? grants.get(frontmost.bundleId) : undefined;
if (tier === undefined) return appNotGranted(frontmost);
let frontmost = await executor.getFrontmostApp();
let grants = new Map(allowedApps.map(app => [app.bundleId, app.tier]));

let tier = frontmost ? grants.get(frontmost.bundleId) : undefined;
if (tier === undefined) return appNotGranted(frontmost);
let frontmost = await executor.getFrontmostApp();
let grants = new Map(allowedApps.map(app => [app.bundleId, app.tier]));

let tier = frontmost ? grants.get(frontmost.bundleId) : undefined;
if (tier === undefined) return appNotGranted(frontmost);

That check worked correctly when iTerm was already frontmost.

The Input Loop

The function identified as _Ly called the application check once and then entered a loop over the complete text:

let denied = await checkFrontmostApplication(context, "keyboard");
if (denied) return denied;

let graphemes = splitIntoGraphemes(text);

for (let grapheme of graphemes) {
  await delay(interKeyDelay);
  if (grapheme === "\n" || grapheme === "\r") await executor.key("return");
  else if (grapheme === "\t") await executor.key("tab");
  else await executor.type(grapheme, { viaClipboard: false });
}
let denied = await checkFrontmostApplication(context, "keyboard");
if (denied) return denied;

let graphemes = splitIntoGraphemes(text);

for (let grapheme of graphemes) {
  await delay(interKeyDelay);
  if (grapheme === "\n" || grapheme === "\r") await executor.key("return");
  else if (grapheme === "\t") await executor.key("tab");
  else await executor.type(grapheme, { viaClipboard: false });
}
let denied = await checkFrontmostApplication(context, "keyboard");
if (denied) return denied;

let graphemes = splitIntoGraphemes(text);

for (let grapheme of graphemes) {
  await delay(interKeyDelay);
  if (grapheme === "\n" || grapheme === "\r") await executor.key("return");
  else if (grapheme === "\t") await executor.key("tab");
  else await executor.type(grapheme, { viaClipboard: false });
}

No check inside the loop verified that Grapher remained frontmost. Return could close a sheet, close a window, quit an application, or submit an action that moved focus elsewhere, yet every later event inherited the original authorization.

The affected implementation checks Grapher once before entering the type loop. The loop later emits Return and additional graphemes after iTerm becomes frontmost, without revalidating the recipient.

The mismatch can be stated precisely:

checked principal:  the frontmost application before the type loop
consumed principal: whichever application owns focus for each emitted event
checked principal:  the frontmost application before the type loop
consumed principal: whichever application owns focus for each emitted event
checked principal:  the frontmost application before the type loop
consumed principal: whichever application owns focus for each emitted event

The approval was valid at the time of the check. It became invalid when the event recipient changed, but the operation had no mechanism to notice.

Controlled Reproduction

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

Item

Verified value

Product

Claude Code

Platform

macOS arm64

Affected version

2.1.226

Affected binary SHA-256

013a1cf17df5ff1dcc189d5d6fd3fdd5f097ddc3cd41aa9992e99805574febbe

Extracted JavaScript SHA-256

a96b5f06a9feba9ff8f7ce7f938a7bd1cbc74c7c03ea0c94f68c50dd05a14f7c

Approved application

Grapher only

Unapproved sink

iTerm2

A localhost Messages API fixture supplied deterministic Computer Use actions. It did not call a real model, retain an authorization header, patch Claude Code, or contact a production service. The proof created only a temporary local marker and cleaned up its dedicated Grapher and iTerm state.

Direct iTerm Negative Control

With iTerm frontmost, we requested the same Computer Use input directly. Claude Code rejected it because iTerm was not in the approved application list. No marker was created. This control established three important facts: the per-application gate was active; iTerm had not been accidentally approved; and the payload did not execute without the lifecycle transition.

Grapher Transition Candidate

The candidate began with Grapher's Save-and-Quit sheet frontmost. The one type call then crossed this sequence:

Grapher save sheet
  -> first Return confirms save and quit
  -> Grapher terminates
  -> macOS restores iTerm focus
  -> remaining graphemes reach iTerm
  -> final Return executes the harmless command
Grapher save sheet
  -> first Return confirms save and quit
  -> Grapher terminates
  -> macOS restores iTerm focus
  -> remaining graphemes reach iTerm
  -> final Return executes the harmless command
Grapher save sheet
  -> first Return confirms save and quit
  -> Grapher terminates
  -> macOS restores iTerm focus
  -> remaining graphemes reach iTerm
  -> final Return executes the harmless command

All three clean runs produced the CUCANARY marker. The grant list still contained only Grapher after execution.

No-Transition Control

The matched control used the same payload, the same Grapher grant, and the same native save sheet, but it did not take the quit path. Grapher remained frontmost, the input stayed inside Grapher, and the iTerm marker remained absent. This control isolated the security-relevant variable: whether the approved recipient changed during the call.

The differential matrix shows direct iTerm input being denied, the Grapher-to-iTerm transition reaching shell execution in three clean runs, and the matched no-transition control remaining inside Grapher.

Case

App at Authorization

App After First Return

Result

Direct iTerm control

iTerm, unapproved

iTerm

Blocked with app_not_granted; no marker

Grapher transition, run 1

Grapher, approved

iTerm, unapproved

Marker created

Grapher transition, run 2

Grapher, approved

iTerm, unapproved

Marker created

Grapher transition, run 3

Grapher, approved

iTerm, unapproved

Marker created

No-transition control

Grapher, approved

Grapher, approved

Input stays in Grapher; no marker

Capability Delta and Impact

The capability delta was not simply that focus changed. It was that a payload denied at the terminal boundary became executable after starting in a lower-risk approved application:

before the transition:
  direct iTerm input is denied

after stale authorization is retained:
  the same operation delivers trusted keyboard events
  to iTerm and reaches host shell execution
before the transition:
  direct iTerm input is denied

after stale authorization is retained:
  the same operation delivers trusted keyboard events
  to iTerm and reaches host shell execution
before the transition:
  direct iTerm input is denied

after stale authorization is retained:
  the same operation delivers trusted keyboard events
  to iTerm and reaches host shell execution

A model-controlled or prompt-injected Computer Use sequence could therefore convert approval for an ordinary application into terminal authority that the user had deliberately not granted. Once input reaches a live shell, it can act with the user's normal host privileges, including reading user-accessible data, changing code or configuration, starting processes, and making network requests.

The issue was not unconditional remote code execution. Computer Use had to be enabled, the user had to approve Grapher for the session, an iTerm window had to be available to receive focus, and attacker-influenced model input had to steer the sequence. These requirements explain the AT:P and UI:P metrics in Anthropic's assessment. They do not remove the bypass of the explicit terminal approval boundary.

Anthropic assigned this CVSS 4.0 vector:

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)

Fix and Independent Retest

Anthropic released a fix in Claude Code 2.1.243. The retest used the stock signed macOS arm64 binary with SHA-256:

1f72dc749e59d1e8b1aa0ecdc2b3a65e599698af9087be4ac1054f57602c304d
1f72dc749e59d1e8b1aa0ecdc2b3a65e599698af9087be4ac1054f57602c304d
1f72dc749e59d1e8b1aa0ecdc2b3a65e599698af9087be4ac1054f57602c304d

The original reproducer was retained. Only the expected affected version and result changed. Grapher remained the sole approved application, direct iTerm input remained denied, and the same Save-and-Quit lifecycle still transferred focus to iTerm.

In three clean retest runs, Claude Code detected the recipient change and stopped the type operation after 66 of 765, 65 of 765, and 66 of 765 graphemes. The remaining command text was not delivered, the final Return did not reach iTerm, and the marker was absent in every run.

The matched no-transition control kept Grapher frontmost. All 765 graphemes completed normally and no iTerm marker appeared. This confirmed that the fix did not simply disable type; it terminated the operation specifically when the approved recipient ceased to be frontmost.

The same Grapher lifecycle succeeds on 2.1.226 because authorization is checked only once. On 2.1.243, Claude Code detects the focus transfer to unapproved iTerm, aborts the remaining keystrokes, and prevents command execution.

Are You Affected?

The vulnerable behavior is in Claude Code 2.1.226 on macOS with Computer Use enabled, and it was fixed in 2.1.243. If you use Computer Use, confirm you are on 2.1.243 or later, where a type operation stops as soon as the approved application loses focus. Check your version with claude --version; standard auto-update should already have carried the fix. Until you have confirmed the fixed version, be deliberate about running Computer Use where an approved application's normal quit or close can hand focus to an unapproved terminal or IDE.

Remediation Principles

The immediate fix is to stop emitting input once the authorized application is no longer the recipient. A robust design should enforce several related properties.

  1. Revalidate every emitted event. Check the recipient before each grapheme, Return, Tab, click, or repeated key event.

  2. Bind more than the display name. Track the bundle identifier, process identity, window, and relevant accessibility target so a replacement or lifecycle transition cannot inherit approval.

  3. Abort on recipient change. Do not continue after a focus transition and do not silently move the remaining input to another application.

  4. Treat multi-event calls as sequences of security decisions. A single tool invocation is not a single recipient when it can emit hundreds of native events.

  5. Test lifecycle-changing input. Regression coverage should include save sheets, quit dialogs, window closure, application launch, modal dismissal, registered file handlers, and other actions that can change the frontmost principal.

  6. Apply the invariant to composite actions. Batch input, held keys, repeated keys, paste operations, drag sequences, and keyboard shortcuts require the same recipient continuity.

Lessons for Agentic Desktop Security

This vulnerability illustrates a common authorization error in systems that translate one high-level AI action into many native operations.

Approval Must Follow the Consumed Object

The user approved Grapher, not a sequence of bytes independent of its destination. Once iTerm became the recipient, the original decision no longer described the action being performed.

Tool-Call Boundaries Are Not Always Security Boundaries

One type call appeared atomic at the protocol layer but was implemented as hundreds of separately delivered events. Security checks placed only at the protocol boundary could not account for state changes inside the loop.

Deterministic Lifecycle Transitions Make Stronger Evidence

A stock Save-and-Quit sequence removed the ambiguity of a timing race. The authorized operation itself caused the transition, which made the checked-versus-consumed mismatch directly reproducible.

Negative Controls Must Prove the New Capability

The direct iTerm denial showed that shell input was protected. The no-transition control showed that the payload did not reach iTerm while Grapher stayed alive. Together, they established that stale authorization, rather than test setup, granted the new capability.

Disclosure Timeline

Date

Event

11 August 2026

Report #3930758 submitted to Anthropic through HackerOne

12 August 2026

Anthropic reproduced the code-level issue and triaged it High at 7.7

25 August 2026

Anthropic released the fix in Claude Code 2.1.243 and requested a retest

25 August 2026

Three affected-path retests failed safely and the matched control remained functional

26 August 2026

Anthropic approved the retest, awarded the bounty, and resolved the report

At the editorial cutoff, the report had not been publicly disclosed and no CVE identifier or public advisory was available.

Conclusion

Claude Code correctly rejected direct input to unapproved iTerm, but the affected type implementation did not preserve that decision when focus changed inside the operation. By checking once and consuming many times, it allowed approval for Grapher to survive beyond Grapher's lifetime and reach a shell-capable application.

The durable security rule is concise:

Every native input event must be authorized against the application that will receive that event, not the application that happened to be frontmost when a longer operation began.

Anthropic's 2.1.243 fix restored that invariant in the tested path by stopping the remaining keystrokes as soon as Grapher ceased to be frontmost.

References:

This research is part of an ongoing effort by CodeAnt AI Security Research to audit the trust boundaries in agentic development tools. It was conducted under Anthropic's vulnerability disclosure program; the proof of concept used stock local applications, a loopback fixture, and a harmless temporary marker, and no third-party data or production service was accessed.

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

BOOK A DEMO

[FAQ]

Frequently Asked
Questions

Is Claude Code Computer Use safe to use?

What was the Claude Code focus-transition (type) vulnerability?

Which Claude Code versions are affected, and is there a CVE?

What is a stale-authorization or checked-versus-consumed (TOCTOU) bug?

How did Anthropic fix it?

How can developers prevent this class of bug in agentic desktop tools?

Can a prompt injection trigger this Claude Code Computer Use bug?

[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