Skip to content

Surface the invoking AI agent on the CLI's User-Agent - #3632

Open
noamshemesh wants to merge 1 commit into
masterfrom
AGW-86-agent-user-agent
Open

Surface the invoking AI agent on the CLI's User-Agent#3632
noamshemesh wants to merge 1 commit into
masterfrom
AGW-86-agent-user-agent

Conversation

@noamshemesh

Copy link
Copy Markdown
Collaborator

What

jfrog-cli-core already detects which AI harness invoked the CLI, from a table of harness
environment variables — Claude Code, Gemini, Goose, Cursor, Copilot, Kilocode, Roo Code, Codex,
plus a generic AGENT fallback collapsed to unknown (common/commands/execution_context.go).

That answer never reached the wire. Its five call sites are telemetry labels, interactive-prompt
suppression, AI-formatted help, the survey link, and the Cursor trace ID — none of them touch the
outbound request. So an agent running jf rt download and a human running it are byte identical to
anything in the request path.

This appends the detected agent to the CLI's User-Agent as an RFC 9110 comment:

User-Agent: jfrog-cli-go/2.117.0 (agent:claude)

Why a comment rather than a new header or a rewritten token

  • The product token stays first, so anything parsing only the leading token is unaffected.
  • A non-agent invocation is byte identical to today — no behaviour change for human use.
  • A dedicated header would need jfrog-client-go. The only global per-request header hook there
    is addUberTraceIdHeaderIfSet, so a new header means a second repo and a release the CLI then
    bumps to. That remains the durable form; this lands the same signal now with no new plumbing.
  • JFROG_CLI_USER_AGENT overrides are preserved — the marker is appended to whatever the override
    resolves to, never replacing it.

Worth noting: execution_context.go already lists "User-Agent enrichment" among its call sites in
the memoization comment, but no such call site existed. This looks like intent that never landed.

Safety

Injection-safe by construction: DetectExecutionContext returns a name from a fixed table or the
literal unknown, so a raw environment value is never propagated onto the wire. A test asserts no
CR/LF can appear.

The value is attribution metadata, not a credential. Harness environment variables are
client-set and trivially forged or unset. Consumers must treat it as a routing/census hint only —
it must never gate authorization.

Tests

15 unit tests in utils/cliutils:

  • one case per row of the agentEnvDetectors table, plus the generic AGENTunknown fallback
  • no agent detected → User-Agent byte identical to today
  • JFROG_CLI_USER_AGENT override preserved with the marker appended
  • no-version case (GetCliUserAgent omits the slash)
  • well-formedness: product token first, no CR/LF

Tests clear the agent env vars and reset the memoized execution context, so they are deterministic
even when the suite itself runs inside Claude Code or Cursor.

go build ./... and go vet clean.

Downstream

JFrog Agent Gateway consumes this marker to attribute diverted requests to a specific harness
rather than a generic "agent", verified end-to-end against a live JPD. That side is already open as
a separate PR and is inert until this ships.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Comment thread utils/cliutils/utils_test.go Outdated
t.Setenv("CLAUDECODE", "true")
corecommands.ResetExecutionContextForTest()

assert.Equal(t, "my-wrapper/9.9.9 (agent:claude)", GetCliUserAgentWithAgent())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isnt "agent/claude" more conventional ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe also ai-agent/claude ? wdyt?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes better

jfrog-cli-core already detects which AI harness invoked the CLI, from a table of
harness environment variables (Claude Code, Gemini, Goose, Cursor, Copilot,
Kilocode, Roo Code, Codex, plus a generic AGENT fallback collapsed to
"unknown"). That answer never reached the wire: it fed telemetry labels, prompt
suppression, AI-formatted help and the survey link, but not the request. So an
agent running "jf rt download" and a human running it were byte identical to
any server in the path.

GetCliUserAgentWithAgent appends the detected agent as an additional product
token, so the wire value becomes:

    jfrog-cli-go/2.117.0 ai-agent/claude

A product token rather than a comment: it is the native User-Agent shape
(compare "Mozilla/5.0 ... Chrome/120 Safari/537.36"), so anything that splits on
whitespace and reads name/version pairs surfaces it as a structured component
instead of discarding it as comment text. "ai-agent" rather than "agent",
because a bare "agent" would collide with this codebase's existing use of the
word for the CLI itself (SetCliUserAgentName, build-info's agent name).

The product-version slot deliberately carries the harness name, not a version:
the execution-context detector exposes no harness version. If one is ever
wanted it belongs in its own token rather than crammed in here.

The leading product token is unchanged and stays first, so anything parsing only
that is unaffected, and a non-agent invocation is byte identical to today.
JFROG_CLI_USER_AGENT overrides are preserved — the marker is appended to
whatever the override resolves to, never replacing it. Injection-safe by
construction: DetectExecutionContext returns a name from a fixed table or the
literal "unknown", so a raw environment value is never propagated.

Note that execution_context.go already lists "User-Agent enrichment" among its
call sites in a comment, but no such call site existed; this appears to be the
intent that never landed.

The value is attribution metadata, not a credential: harness environment
variables are client-set and trivially forged or unset, so consumers must treat
it as a routing or census hint only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@noamshemesh

Copy link
Copy Markdown
Collaborator Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants