Living-off-the-Agent: Coding Tunnels and Persistence
An adversary is utilizing trusted, signed AI coding agents (Claude Code, Cursor) to execute shells, establish reverse tunnels, and maintain persistence via LaunchAgents on developer workstations.
Based on research by Elastic Security Labs 2026-09-17 9 steps · 4 queries T1133 T1218 T1555.001 T1566 T1572
Brief
Why this hunt?
Elastic
Security Labs recently published a detailed analysis titled "Living off the coding agent: Two tales of tunnels and LaunchAgents" (https://www.elastic.co/security-labs/threat-command/coding-agent-launchagent-tunnel-detection). Their research demonstrates how signed AI coding agents, which developers increasingly trust with high-level system access, can be manipulated to execute shells, establish reverse tunnels, and even install persistence. Because these agents are often signed and widely used, traditional alerts based on process name or signature can be noisy. This hunt provides a structured way to verify that your developer workstations haven't been turned into remote access points through the very tools meant to increase productivity.
How the hunt flows
The hunt begins with a scoping phase using the hb_software_inventory surface
We look for the presence of known AI agents like Claude and Cursor. This allows us to focus our analysis on the specific population of developer machines where this threat is most relevant, rather than scanning the entire fleet for generic shell activity.
Next, we pivot to hb_process_activity to identify suspicious execution patterns. We look for shells (zsh, bash) or utility binaries (curl, cloudflared) where the parent process is the coding agent. We specifically look for dangerous flags mentioned in the source article, such as --allow-dangerously-skip-permissions, which indicates the agent is operating with reduced guardrails.
We then examine hb_network_connection to identify connections to rare tunnel brokers. By stacking connections to domains like lhr.life, trycloudflare.com, or ngrok-free.app and filtering for low prevalence across the fleet, we can isolate specific workstations that are communicating with ephemeral remote management infrastructure.
The final technical step involves checking hb_scheduled_job for anomalous LaunchAgent persistence. We search for new plist entries or job definitions created within the last 14 days that reference tunnel binaries. This step is crucial for identifying sessions that have transitioned from interactive experimentation to a persistent foothold.
What the hunt cannot see
This hunt has two primary blind spots
First, if your EDR does not support deep process ancestry tracking or if the process tree is truncated, the link between the trusted agent and the malicious child shell may be lost. Second, because tunnel brokers use randomized subdomains, we can identify the use of the broker but cannot necessarily correlate specific tunnel instances without high-resolution DNS or HTTP logging.
Steps
-
Identify hosts with AI coding agents
Query · scopingFocus the hunt on machines where agents like Claude Code or Cursor are installed using partial match on package and vendor names.
reads hb_software_inventorysqlSELECT DISTINCT device_hostname FROM hb_software_inventory WHERE (LOWER(package_name) LIKE '%claude%' OR LOWER(package_name) LIKE '%cursor%' OR LOWER(vendor_name) LIKE '%anthropic%' OR LOWER(vendor_name) LIKE '%anysphere%')What a hit looks like. A list of hostnames belonging to developers using these agents. Silence suggests the agents are installed as portable binaries outside the package manager.
-
Agent-parented shell and tunnel execution
Query · detection candidateIdentify shells, credentialed curl commands, or tunnel binaries spawned by the AI agent using partial path matching.
reads hb_process_activitysqlSELECT device_hostname, process_name, process_cmd_line, parent_process_name, time FROM hb_process_activity WHERE (LOWER(parent_process_name) LIKE '%claude%' OR LOWER(parent_process_name) LIKE '%cursor%' OR LOWER(process_name) LIKE '%claude%' OR LOWER(process_name) LIKE '%cursor%') AND (LOWER(process_name) LIKE '%/zsh' OR LOWER(process_name) LIKE '%/bash' OR LOWER(process_name) LIKE '%/python%' OR LOWER(process_name) LIKE '%/curl' OR LOWER(process_name) LIKE '%/cloudflared' OR LOWER(process_name) LIKE '%/ngrok' OR LOWER(process_cmd_line) LIKE '%--allow-dangerously-skip-permissions%' OR LOWER(process_cmd_line) LIKE '%security dump-keychain%' OR LOWER(process_cmd_line) LIKE '%user=%') AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')What a hit looks like. Process trees where Claude or Cursor spawn shells with sensitive flags or credential-heavy command lines. This is the primary signal for 'living-off-the-agent' proxying.
-
Rare tunnel broker connections
Query · baselineStack-count connections to known tunnel brokers, filtering for domain-based activity to identify rare usage.
reads hb_network_connectionsqlSELECT dst_endpoint_hostname, COUNT(DISTINCT device_hostname) AS hosts, COUNT(*) AS connections, MIN(time) AS first_seen FROM hb_network_connection WHERE dst_endpoint_hostname IS NOT NULL AND instr(',' || '{{tunnel_domains}}' || ',', ',' || LOWER(dst_endpoint_hostname) || ',') > 0 AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY dst_endpoint_hostname HAVING hosts <= 3 ORDER BY hosts ASCWhat a hit looks like. A connection to a tunnel provider that stands out from fleet norms. Domain matching avoids noise from raw IP background traffic.
-
Anomalous LaunchAgent persistence
Query · enrichmentCheck for new macOS persistence mechanisms that maintain tunnel connections outside of active sessions.
reads hb_scheduled_jobsqlSELECT device_hostname, job_name, job_definition_path, job_cmd_line, time FROM hb_scheduled_job WHERE job_kind = 'launchd' AND (LOWER(job_definition_path) LIKE '%/launchagents/%' OR LOWER(job_cmd_line) LIKE '%cloudflared%' OR LOWER(job_cmd_line) LIKE '%ngrok%' OR LOWER(job_cmd_line) LIKE '%lhr.life%') AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')What a hit looks like. LaunchAgent plists pointing to tunnel binaries or shell wrappers. Legitimate developers rarely install these for temporary testing.
-
Triage agent-parented chains
Agent triageWeigh the correlation of ancestry, tunnel usage, and persistence to distinguish legitimate dev-ops from a remote intrusion.
-
Route on verdict
DecisionDirect high-confidence findings to containment and suspicious findings to human review.
-
Isolate workstation
Response actionHalt the potential reverse tunnel and prevent lateral movement or further exfiltration.
-
Analyst review
Analyst taskFinal human review to confirm developer intent and close the case.
Coverage
Scenario coverage
| Stage | Covered | How, or why not |
|---|---|---|
| Coding Agent Shell Execution T1218 |
Yes | agent-parented-behavior |
| Credential Access and Harvesting T1555.001 |
Yes | agent-parented-behavior |
| Reverse Tunnel C2 T1572 |
Yes | tunnel-prevalence, agent-parented-behavior |
| External Service Persistence T1133 |
Yes | persistence-check |
Blind spots
- Needs EDR process tree tracking with full path depth. If the parent process is missing or truncated, the critical link between the trusted agent and the malicious child is lost, potentially misclassifying the alert as a generic user activity. It would answer Was the tunnel binary spawned directly by the agent or by an unrelated user shell?. Remediation: Audit EDR configuration to ensure parent command lines and full executable paths are captured for all process launch events.
- Needs hb_dns_activity or hb_http_activity with full URL. Tunnel brokers use randomized subdomains. If only the apex domain is logged, we cannot isolate the specific tunnel instance for cross-host correlation. It would answer What was the specific subdomain for the lhr.life tunnel?. Remediation: Enable DNS query logging for all developer VLANs.
Parameters & data
Parameters
| Parameter | Type | Default | What it is |
|---|---|---|---|
lookback_days | number | 14 | Days of history to examine. |
scope_hosts | list[host] | — | Optional: Hostnames to restrict the hunt to after the scoping step. |
tunnel_domains | list[domain] | lhr.life, localhost.run, trycloudflare.com, api.trycloudflare.com, ngrok-free.app, ngrok.com | Common domains for free or ephemeral tunnel brokers. |
Telemetry
| Source | Category | Telemetry |
|---|---|---|
| Endpoint telemetry (hb_ surfaces) | endpoint | endpoint |
| Network telemetry | network | network |
Source
---
analysis: A single rule on tunnel binaries (cloudflared, ngrok) would be too noisy
for developers. This hunt correlates the trusted agent ancestry with rare networking
behaviors and persistence, using a baseline to filter out fleet-wide dev-ops noise.
blind_spots:
- id: insufficient-process-ancestry-tracking
owner: Endpoint Security Team
question: Was the tunnel binary spawned directly by the agent or by an unrelated
user shell?
remediation: Audit EDR configuration to ensure parent command lines and full executable
paths are captured for all process launch events.
requires: EDR process tree tracking with full path depth
risk: If the parent process is missing or truncated, the critical link between the
trusted agent and the malicious child is lost, potentially misclassifying the
alert as a generic user activity.
stage: agent-parented-shell-execution
- id: ephemeral-tunnel-subdomains
owner: Network Engineering
question: What was the specific subdomain for the lhr.life tunnel?
remediation: Enable DNS query logging for all developer VLANs.
requires: hb_dns_activity or hb_http_activity with full URL
risk: Tunnel brokers use randomized subdomains. If only the apex domain is logged,
we cannot isolate the specific tunnel instance for cross-host correlation.
stage: reverse-tunnel-establishment
coverage:
- stage: agent-parented-shell-execution
status: covered
steps:
- agent-parented-behavior
- reason: Covers credential-harvesting command patterns like 'security dump-keychain'
and plaintext POST parameters.
stage: credential-access-and-exfiltration
status: covered
steps:
- agent-parented-behavior
- stage: reverse-tunnel-establishment
status: covered
steps:
- tunnel-prevalence
- agent-parented-behavior
- stage: launchagent-persistence
status: covered
steps:
- persistence-check
guardrails:
claims: no_unsupported
evidence: citation_required
missing_data: not_benign
telemetry: untrusted
hunt:
applicability: campaign-specific
handoff: promote-to-detection
justification: AI coding agents operate with significant local trust and can automate
complex tasks. Their use as a proxy for remote management bypasses traditional
firewall controls and session monitoring, making a negative result over developer
machines an essential security verification.
methodology: model-assisted
trigger: intel-report
hypothesis: An adversary is utilizing trusted, signed AI coding agents (Claude Code,
Cursor) to execute shells, establish reverse tunnels, and maintain persistence via
LaunchAgents on developer workstations.
labels:
- hunt
- attack.t1133
- attack.t1218
- attack.t1555.001
- attack.t1572
- attack.t1566
name: 'Living-off-the-Agent: Coding Tunnels and Persistence'
parameters:
lookback_days:
default: '14'
description: Days of history to examine.
type: number
scope_hosts:
default: []
description: 'Optional: Hostnames to restrict the hunt to after the scoping step.'
type: list[host]
tunnel_domains:
default:
- lhr.life
- localhost.run
- trycloudflare.com
- api.trycloudflare.com
- ngrok-free.app
- ngrok.com
description: Common domains for free or ephemeral tunnel brokers.
from:
kind: article
observed: '2026-08-07'
ref: elastic-labs-coding-agent
type: list[domain]
provenance:
authors:
- name: Huntbase hunt generation
org: huntbase.io
generated:
by: huntbase-hunt-generation
from: https://www.elastic.co/security-labs/threat-command/coding-agent-launchagent-tunnel-detection
gates:
- dry-run
- lint
model: hb_google/gemini-3-flash-preview
rationale: Focus on workstations assigned to software engineering and data science
teams. AI agents like Claude Code are often installed in user-profile paths, so
behavioral queries use partial string matches to account for variable install locations.
references:
- name: 'Living off the coding agent: Two tales of tunnels and LaunchAgents'
url: https://www.elastic.co/security-labs/threat-command/coding-agent-launchagent-tunnel-detection
related:
- hunt: mcp-server-anomalies
reason: Model Context Protocol (MCP) servers represent another vector for local
tool execution under agent control.
relation: sibling
scenario:
stages:
- name: Coding Agent Shell Execution
observables:
- Claude Code
- Cursor
- zsh
- --allow-dangerously-skip-permissions
- /tmp/mcp_clean_landers.py
slug: agent-parented-shell-execution
tactic: execution
techniques:
- T1218
- name: Credential Access and Harvesting
observables:
- security dump-keychain
- curl -X POST
- user=
- password=
- /login
- lhr.life
- trycloudflare.com
slug: credential-access-and-exfiltration
tactic: credential-access
techniques:
- T1555.001
- name: Reverse Tunnel C2
observables:
- cloudflared
- ngrok
- localhost.run
- lhr.life
- trycloudflare.com
- api.trycloudflare.com
slug: reverse-tunnel-establishment
tactic: command-and-control
techniques:
- T1572
- name: External Service Persistence
observables:
- LaunchAgents
- .plist
- launchd
- ~/.zshenv
slug: launchagent-persistence
tactic: persistence
techniques:
- T1133
summary: This campaign involves the abuse of trusted coding agents like Claude Code
and Cursor to proxy malicious activities, including credential harvesting from
the macOS Keychain and the deployment of reverse tunnels using tools like cloudflared
and ngrok. The attackers establish persistent remote access to local services
via LaunchAgents, effectively bypassing firewall restrictions under the guise
of legitimate developer operations.
severity: high
targets:
analyst:
name: Tier-2 analyst
role: analyst
endpoint:
category: endpoint
name: Endpoint telemetry (hb_ surfaces)
telemetry:
- endpoint
hunter:
agent: true
name: Hunt agent
network:
category: network
name: Network telemetry
telemetry:
- network
tlp: clear
type: investigation
---
# Living-off-the-Agent: Coding Tunnels and Persistence
This hunt targets 'vibe-coded' intrusions where a developer workstation's trust in signed AI agents is abused to create remote access. By scoping to hosts running these agents and looking for correlated signals—agent-parented shells with dangerous flags, connections to free tunnel brokers (lhr.life, trycloudflare), and new LaunchAgent persistence—this hunt identifies sessions where an agent is used as a living-off-the-land execution proxy. It focuses on the behavioral overlap between legitimate developer automation and unauthorized remote management.
## scoping-agent-hosts
<!-- Identify hosts with AI coding agents -->
Focus the hunt on machines where agents like Claude Code or Cursor are installed using partial match on package and vendor names.
```sqlite target=endpoint role=scoping
~~~yaml
expected: A list of hostnames belonging to developers using these agents. Silence
suggests the agents are installed as portable binaries outside the package manager.
reads:
- device_hostname
- package_name
- vendor_name
silence: not_evidence_of_absence
source: hb_software_inventory
verified: dry-run
verified_at: '2026-09-17'
~~~
SELECT DISTINCT device_hostname FROM hb_software_inventory WHERE (LOWER(package_name) LIKE '%claude%' OR LOWER(package_name) LIKE '%cursor%' OR LOWER(vendor_name) LIKE '%anthropic%' OR LOWER(vendor_name) LIKE '%anysphere%')
```
## corroborate-activity
<!-- Parallel evidence collection -->
parallel:
- → agent-parented-behavior
- → tunnel-prevalence
- → persistence-check
join: → triage
## agent-parented-behavior
<!-- Agent-parented shell and tunnel execution -->
Identify shells, credentialed curl commands, or tunnel binaries spawned by the AI agent using partial path matching.
```sqlite target=endpoint role=detection-candidate params=(scope_hosts=scope_hosts, lookback_days=lookback_days)
~~~yaml
expected: Process trees where Claude or Cursor spawn shells with sensitive flags or
credential-heavy command lines. This is the primary signal for 'living-off-the-agent'
proxying.
reads:
- device_hostname
- process_name
- process_cmd_line
- parent_process_name
- time
silence: not_evidence_of_absence
source: hb_process_activity
verified: dry-run
verified_at: '2026-09-17'
~~~
SELECT device_hostname, process_name, process_cmd_line, parent_process_name, time FROM hb_process_activity WHERE (LOWER(parent_process_name) LIKE '%claude%' OR LOWER(parent_process_name) LIKE '%cursor%' OR LOWER(process_name) LIKE '%claude%' OR LOWER(process_name) LIKE '%cursor%') AND (LOWER(process_name) LIKE '%/zsh' OR LOWER(process_name) LIKE '%/bash' OR LOWER(process_name) LIKE '%/python%' OR LOWER(process_name) LIKE '%/curl' OR LOWER(process_name) LIKE '%/cloudflared' OR LOWER(process_name) LIKE '%/ngrok' OR LOWER(process_cmd_line) LIKE '%--allow-dangerously-skip-permissions%' OR LOWER(process_cmd_line) LIKE '%security dump-keychain%' OR LOWER(process_cmd_line) LIKE '%user=%') AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')
```
## tunnel-prevalence
<!-- Rare tunnel broker connections -->
Stack-count connections to known tunnel brokers, filtering for domain-based activity to identify rare usage.
```sqlite target=network role=baseline params=(tunnel_domains=tunnel_domains, scope_hosts=scope_hosts, lookback_days=lookback_days)
~~~yaml
baseline:
compare: first_seen
window: '{{lookback_days}}d'
expected: A connection to a tunnel provider that stands out from fleet norms. Domain
matching avoids noise from raw IP background traffic.
prevalence:
by: device_hostname
key:
- dst_endpoint_hostname
rare_below: 3
reads:
- dst_endpoint_hostname
- device_hostname
- time
silence: not_evidence_of_absence
source: hb_network_connection
verified: dry-run
verified_at: '2026-09-17'
~~~
SELECT dst_endpoint_hostname, COUNT(DISTINCT device_hostname) AS hosts, COUNT(*) AS connections, MIN(time) AS first_seen FROM hb_network_connection WHERE dst_endpoint_hostname IS NOT NULL AND instr(',' || '{{tunnel_domains}}' || ',', ',' || LOWER(dst_endpoint_hostname) || ',') > 0 AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days') GROUP BY dst_endpoint_hostname HAVING hosts <= 3 ORDER BY hosts ASC
```
## persistence-check
<!-- Anomalous LaunchAgent persistence -->
Check for new macOS persistence mechanisms that maintain tunnel connections outside of active sessions.
```sqlite target=endpoint role=enrichment params=(scope_hosts=scope_hosts, lookback_days=lookback_days)
~~~yaml
expected: LaunchAgent plists pointing to tunnel binaries or shell wrappers. Legitimate
developers rarely install these for temporary testing.
reads:
- device_hostname
- job_name
- job_definition_path
- job_cmd_line
- time
silence: not_evidence_of_absence
source: hb_scheduled_job
verified: dry-run
verified_at: '2026-09-17'
~~~
SELECT device_hostname, job_name, job_definition_path, job_cmd_line, time FROM hb_scheduled_job WHERE job_kind = 'launchd' AND (LOWER(job_definition_path) LIKE '%/launchagents/%' OR LOWER(job_cmd_line) LIKE '%cloudflared%' OR LOWER(job_cmd_line) LIKE '%ngrok%' OR LOWER(job_cmd_line) LIKE '%lhr.life%') AND ('{{scope_hosts}}' = '' OR instr(',' || '{{scope_hosts}}' || ',', ',' || device_hostname || ',') > 0) AND time >= datetime('now', '-{{lookback_days}} days')
```
## triage
<!-- Triage agent-parented chains -->
```agent target=hunter
cite: required
context:
- agent-parented-behavior
- tunnel-prevalence
- persistence-check
max_iterations: 6
objective: Determine if the observed behavior represents unauthorized remote access
via a coding agent. Specifically look for credentials POSTed to tunnel URLs combined
with persistent LaunchAgents.
success_criteria: A per-host verdict citing specific process command lines and tunnel
domains.
tools:
- endpoint
- network
```
## route
<!-- Route on verdict -->
if~: "the triage verdict is malicious for at least one host" (confidence: high, judge=hunter)
then: → isolate-host
indeterminate: → analyst-review
unavailable: → analyst-review (blind_spot: insufficient-process-ancestry-tracking)
else: → analyst-review
## isolate-host
<!-- Isolate workstation -->
```action target=endpoint
~~~yaml
approval: required
~~~
Isolate the host, terminate the agent-parented tunnel processes, and collect the LaunchAgent plists for forensic analysis.
```
→ analyst-review
## analyst-review
<!-- Analyst review -->
```manual target=analyst
Examine the cited command lines. Determine if the developer was self-testing a local app or if the agent session was manipulated. If malicious, investigate the credentials POSTed to the tunnel URL and check for lateral movement attempts.
```
→ end
Run it
Take this hunt into your environment.
Open it in Huntbase to run every step against your own connections, with Scout weighing the evidence and your analysts in command. Or take the open hunt.md file anywhere that reads the format.
Machine-drafted by huntbase-hunt-generation using hb_google/gemini-3-flash-preview, gated by dry-run, lint, then reviewed by a person.