Record agentic invocations in User-Agent header

Detect which AI coding agent is invoking gh by checking well-known
environment variables and include the agent name in the User-Agent
header sent to GitHub APIs.

Supported agents: Codex, Gemini CLI, Copilot CLI, OpenCode,
Claude Code, and Amp. Generic AI_AGENT env var is also supported
with validation to prevent header injection.

Fixes github/cli#1111

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
William Martin 2026-03-24 17:05:34 +01:00
parent 8723e3bb52
commit c51769c977
10 changed files with 311 additions and 41 deletions

View file

@ -15,7 +15,6 @@ import (
)
const (
accept = "Accept"
apiVersion = "X-GitHub-Api-Version"
apiVersionValue = "2022-11-28"
authorization = "Authorization"

View file

@ -18,6 +18,7 @@ type tokenGetter interface {
type HTTPClientOptions struct {
AppVersion string
InvokingAgent string
CacheTTL time.Duration
Config tokenGetter
EnableCache bool
@ -48,8 +49,13 @@ func NewHTTPClient(opts HTTPClientOptions) (*http.Client, error) {
clientOpts.LogVerboseHTTP = opts.LogVerboseHTTP
}
ua := fmt.Sprintf("GitHub CLI %s", opts.AppVersion)
if opts.InvokingAgent != "" {
ua = fmt.Sprintf("%s Agent/%s", ua, opts.InvokingAgent)
}
headers := map[string]string{
userAgent: fmt.Sprintf("GitHub CLI %s", opts.AppVersion),
userAgent: ua,
apiVersion: apiVersionValue,
}
clientOpts.Headers = headers

View file

@ -20,6 +20,7 @@ func TestNewHTTPClient(t *testing.T) {
type args struct {
config tokenGetter
appVersion string
invokingAgent string
logVerboseHTTP bool
skipDefaultHeaders bool
}
@ -155,6 +156,18 @@ func TestNewHTTPClient(t *testing.T) {
* Request took <duration>
`),
},
{
name: "includes invoking agent in user-agent header",
args: args{
appVersion: "v1.2.3",
invokingAgent: "copilot-cli",
},
host: "github.com",
wantHeader: map[string][]string{
"user-agent": {"GitHub CLI v1.2.3 Agent/copilot-cli"},
},
wantStderr: "",
},
}
var gotReq *http.Request
@ -169,6 +182,7 @@ func TestNewHTTPClient(t *testing.T) {
ios, _, _, stderr := iostreams.Test()
client, err := NewHTTPClient(HTTPClientOptions{
AppVersion: tt.args.appVersion,
InvokingAgent: tt.args.invokingAgent,
Config: tt.args.config,
Log: ios.ErrOut,
LogVerboseHTTP: tt.args.logVerboseHTTP,