Skip to content

Commit 7fc52b6

Browse files
Add AI coding agent detection to User-Agent header
Detect when the Node.js SQL driver is invoked by an AI coding agent (e.g. Claude Code, Cursor, Gemini CLI) by checking well-known environment variables, and append `agent/<product>` to the User-Agent string. This enables Databricks to understand how much driver usage originates from AI coding agents. Detection only succeeds when exactly one agent is detected to avoid ambiguous attribution. Mirrors the approach in databricks/cli#4287. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Vikrant Puppala <vikrant.puppala@databricks.com>
1 parent 538556d commit 7fc52b6

File tree

4 files changed

+99
-2
lines changed

4 files changed

+99
-2
lines changed

lib/utils/agentDetector.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* Detects whether the Node.js SQL driver is being invoked by an AI coding agent
3+
* by checking for well-known environment variables that agents set in their
4+
* spawned shell processes.
5+
*
6+
* Detection only succeeds when exactly one agent environment variable is present,
7+
* to avoid ambiguous attribution when multiple agent environments overlap.
8+
*
9+
* Adding a new agent requires only a new entry in `knownAgents`.
10+
*
11+
* References for each environment variable:
12+
* - ANTIGRAVITY_AGENT: Closed source. Google Antigravity sets this variable.
13+
* - CLAUDECODE: https://github.com/anthropics/claude-code (sets CLAUDECODE=1)
14+
* - CLINE_ACTIVE: https://github.com/cline/cline (shipped in v3.24.0)
15+
* - CODEX_CI: https://github.com/openai/codex (part of UNIFIED_EXEC_ENV array in codex-rs)
16+
* - CURSOR_AGENT: Closed source. Referenced in a gist by johnlindquist.
17+
* - GEMINI_CLI: https://google-gemini.github.io/gemini-cli/docs/tools/shell.html (sets GEMINI_CLI=1)
18+
* - OPENCODE: https://github.com/opencode-ai/opencode (sets OPENCODE=1)
19+
*/
20+
21+
const knownAgents: Array<{ envVar: string; product: string }> = [
22+
{ envVar: 'ANTIGRAVITY_AGENT', product: 'antigravity' },
23+
{ envVar: 'CLAUDECODE', product: 'claude-code' },
24+
{ envVar: 'CLINE_ACTIVE', product: 'cline' },
25+
{ envVar: 'CODEX_CI', product: 'codex' },
26+
{ envVar: 'CURSOR_AGENT', product: 'cursor' },
27+
{ envVar: 'GEMINI_CLI', product: 'gemini-cli' },
28+
{ envVar: 'OPENCODE', product: 'opencode' },
29+
];
30+
31+
export default function detectAgent(env: Record<string, string | undefined> = process.env): string {
32+
const detected = knownAgents.filter((a) => env[a.envVar]).map((a) => a.product);
33+
34+
if (detected.length === 1) {
35+
return detected[0];
36+
}
37+
return '';
38+
}

lib/utils/buildUserAgentString.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os from 'os';
22
import packageVersion from '../version';
3+
import detectAgent from './agentDetector';
34

45
const productName = 'NodejsDatabricksSqlConnector';
56

@@ -27,5 +28,12 @@ export default function buildUserAgentString(userAgentEntry?: string): string {
2728
}
2829

2930
const extra = [userAgentEntry, getNodeVersion(), getOperatingSystemVersion()].filter(Boolean);
30-
return `${productName}/${packageVersion} (${extra.join('; ')})`;
31+
let ua = `${productName}/${packageVersion} (${extra.join('; ')})`;
32+
33+
const agentProduct = detectAgent();
34+
if (agentProduct) {
35+
ua += ` agent/${agentProduct}`;
36+
}
37+
38+
return ua;
3139
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { expect } from 'chai';
2+
import detectAgent from '../../../lib/utils/agentDetector';
3+
4+
describe('detectAgent', () => {
5+
const allAgents = [
6+
{ envVar: 'ANTIGRAVITY_AGENT', product: 'antigravity' },
7+
{ envVar: 'CLAUDECODE', product: 'claude-code' },
8+
{ envVar: 'CLINE_ACTIVE', product: 'cline' },
9+
{ envVar: 'CODEX_CI', product: 'codex' },
10+
{ envVar: 'CURSOR_AGENT', product: 'cursor' },
11+
{ envVar: 'GEMINI_CLI', product: 'gemini-cli' },
12+
{ envVar: 'OPENCODE', product: 'opencode' },
13+
];
14+
15+
for (const { envVar, product } of allAgents) {
16+
it(`detects ${product} when ${envVar} is set`, () => {
17+
expect(detectAgent({ [envVar]: '1' })).to.equal(product);
18+
});
19+
}
20+
21+
it('returns empty string when no agent is detected', () => {
22+
expect(detectAgent({})).to.equal('');
23+
});
24+
25+
it('returns empty string when multiple agents are detected', () => {
26+
expect(detectAgent({ CLAUDECODE: '1', CURSOR_AGENT: '1' })).to.equal('');
27+
});
28+
29+
it('ignores empty env var values', () => {
30+
expect(detectAgent({ CLAUDECODE: '' })).to.equal('');
31+
});
32+
33+
it('ignores undefined env var values', () => {
34+
expect(detectAgent({ CLAUDECODE: undefined })).to.equal('');
35+
});
36+
});

tests/unit/utils/utils.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('buildUserAgentString', () => {
3232
// Prefix: 'NodejsDatabricksSqlConnector/'
3333
// Version: three period-separated digits and optional suffix
3434
const re =
35-
/^(?<productName>NodejsDatabricksSqlConnector)\/(?<productVersion>\d+\.\d+\.\d+(-[^(]+)?)\s*\((?<comment>[^)]+)\)$/i;
35+
/^(?<productName>NodejsDatabricksSqlConnector)\/(?<productVersion>\d+\.\d+\.\d+(-[^(]+)?)\s*\((?<comment>[^)]+)\)(\s+agent\/[a-z-]+)?$/i;
3636
const match = re.exec(ua);
3737
expect(match).to.not.be.eq(null);
3838

@@ -62,6 +62,21 @@ describe('buildUserAgentString', () => {
6262
const userAgentString = buildUserAgentString(userAgentEntry);
6363
expect(userAgentString).to.include('<REDACTED>');
6464
});
65+
66+
it('appends agent suffix when agent env var is set', () => {
67+
const orig = process.env.CLAUDECODE;
68+
try {
69+
process.env.CLAUDECODE = '1';
70+
const ua = buildUserAgentString();
71+
expect(ua).to.include('agent/claude-code');
72+
} finally {
73+
if (orig === undefined) {
74+
delete process.env.CLAUDECODE;
75+
} else {
76+
process.env.CLAUDECODE = orig;
77+
}
78+
}
79+
});
6580
});
6681

6782
describe('formatProgress', () => {

0 commit comments

Comments
 (0)