mirror of
https://github.com/anthropics/claude-code.git
synced 2026-04-19 01:52:42 +00:00
Compare commits
1 Commits
claude/sla
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a1c529e23 |
221
ANT_ONLY_METADATA_USER_ID_SPEC.md
Normal file
221
ANT_ONLY_METADATA_USER_ID_SPEC.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# ANT_ONLY_METADATA_USER_ID Implementation Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document specifies the implementation of the `ANT_ONLY_METADATA_USER_ID` environment variable feature for Claude Code. This feature allows internal Anthropic jobs (like Claude Oracle, Oncall, Ambient, etc.) to pass custom metadata through Claude Code sessions to the Anthropic API for tracking, rate limiting, and incident triage purposes.
|
||||
|
||||
## Background
|
||||
|
||||
From the Slack discussion:
|
||||
- Internal API jobs launched via Claude Code currently lack user metadata showing `job_id` or `username` in `api_usage`
|
||||
- ~85% of requests from heavy CC users (like Closing The Loop org) have no user_metadata or job identifier
|
||||
- This metadata is critical for:
|
||||
- Managing Internal API rate limits
|
||||
- Triaging incidents
|
||||
- Setting up alerting that pings the DRI researchers
|
||||
- Understanding which jobs are driving token usage
|
||||
|
||||
## Requirements
|
||||
|
||||
1. **Environment Variable**: Claude Code must read an environment variable called `ANT_ONLY_METADATA_USER_ID`
|
||||
2. **API Integration**: The value must be passed to all Anthropic Messages API calls in the `metadata.user_id` field
|
||||
3. **Tree Shaking**: Only Anthropic internal builds should be able to use this feature
|
||||
4. **Format**: The expected format is a JSON object: `{"user": "<anthropic_username>", "job": "<job_name>"}`
|
||||
- Example: `{"user": "tedm", "job": "tedm-1208-mu-g-18x"}`
|
||||
- Or: `{"user": "vinke", "job": "claude-oracle:fluorine:vinke"}`
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Environment Variable Reading
|
||||
|
||||
In the appropriate configuration/env module (likely `src/config/env.ts` or similar):
|
||||
|
||||
```typescript
|
||||
// Environment variable for Anthropic internal builds only
|
||||
// This is used to pass job metadata to the API for usage tracking
|
||||
export function getAntOnlyMetadataUserId(): string | undefined {
|
||||
// Only available in Anthropic internal builds
|
||||
if (!isAnthropicBuild()) {
|
||||
return undefined;
|
||||
}
|
||||
return process.env.ANT_ONLY_METADATA_USER_ID;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. API Request Metadata
|
||||
|
||||
When constructing API requests to the Anthropic Messages API, add the metadata:
|
||||
|
||||
```typescript
|
||||
// In the API client module (e.g., src/api/anthropic.ts)
|
||||
import { getAntOnlyMetadataUserId } from '../config/env';
|
||||
|
||||
function buildApiRequestMetadata(): Record<string, string> | undefined {
|
||||
const metadataUserId = getAntOnlyMetadataUserId();
|
||||
|
||||
if (!metadataUserId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
user_id: metadataUserId
|
||||
};
|
||||
}
|
||||
|
||||
// When creating a message:
|
||||
const response = await client.messages.create({
|
||||
model: selectedModel,
|
||||
max_tokens: maxTokens,
|
||||
messages: conversationMessages,
|
||||
// ... other parameters
|
||||
metadata: buildApiRequestMetadata()
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Tree Shaking for Internal Builds
|
||||
|
||||
The feature should be conditionally compiled out for public builds using build-time flags:
|
||||
|
||||
```typescript
|
||||
// Using a build-time constant
|
||||
declare const __ANT_INTERNAL_BUILD__: boolean;
|
||||
|
||||
export function isAnthropicBuild(): boolean {
|
||||
// This will be replaced at build time
|
||||
// For internal builds: true
|
||||
// For public builds: false (and the code will be tree-shaken)
|
||||
if (typeof __ANT_INTERNAL_BUILD__ !== 'undefined') {
|
||||
return __ANT_INTERNAL_BUILD__;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
In the build configuration (esbuild/webpack/rollup):
|
||||
|
||||
```javascript
|
||||
// For public builds:
|
||||
define: {
|
||||
'__ANT_INTERNAL_BUILD__': 'false'
|
||||
}
|
||||
|
||||
// For internal builds:
|
||||
define: {
|
||||
'__ANT_INTERNAL_BUILD__': 'true'
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Validation (Optional but Recommended)
|
||||
|
||||
```typescript
|
||||
function validateMetadataUserId(value: string): boolean {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return typeof parsed.user === 'string' && typeof parsed.job === 'string';
|
||||
} catch {
|
||||
// Also allow simple string format for backward compatibility
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function getValidatedAntOnlyMetadataUserId(): string | undefined {
|
||||
const value = getAntOnlyMetadataUserId();
|
||||
if (value && !validateMetadataUserId(value)) {
|
||||
console.warn('ANT_ONLY_METADATA_USER_ID has invalid format. Expected JSON: {"user": "...", "job": "..."}');
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage by Internal Services
|
||||
|
||||
Services like Claude Oracle should set this environment variable before launching Claude Code sessions:
|
||||
|
||||
```python
|
||||
# In Claude Oracle / coo job code
|
||||
import os
|
||||
import json
|
||||
|
||||
# Set the metadata before spawning Claude Code
|
||||
os.environ['ANT_ONLY_METADATA_USER_ID'] = json.dumps({
|
||||
"user": username, # Anthropic username
|
||||
"job": job_name # e.g., "claude-oracle:fluorine:vinke"
|
||||
})
|
||||
|
||||
# Then launch Claude Code session...
|
||||
```
|
||||
|
||||
Or via command line:
|
||||
|
||||
```bash
|
||||
ANT_ONLY_METADATA_USER_ID='{"user":"tedm","job":"tedm-1208-mu-g-18x"}' claude --headless ...
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ COO Job / Oracle / Oncall / etc. │
|
||||
│ Sets: ANT_ONLY_METADATA_USER_ID='{"user":"..","job":".."}' │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Claude Code CLI │
|
||||
│ Reads: process.env.ANT_ONLY_METADATA_USER_ID │
|
||||
│ Adds to API calls: metadata: { user_id: "..." } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Anthropic API │
|
||||
│ Receives: metadata.user_id in request │
|
||||
│ Stores in: api_usage.metadata_user_id (BigQuery) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ BigQuery: anthropic.api_production.api_usage │
|
||||
│ Fields populated: │
|
||||
│ - metadata_user_id │
|
||||
│ - device_id (if included) │
|
||||
│ - claude_code_session_id (if included) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
1. **Unit Tests**:
|
||||
- Test that `getAntOnlyMetadataUserId()` returns undefined when env var is not set
|
||||
- Test that it returns the value when set (in internal builds)
|
||||
- Test validation of JSON format
|
||||
|
||||
2. **Integration Tests**:
|
||||
- Verify metadata is included in API requests
|
||||
- Verify tree-shaking removes the code in public builds
|
||||
|
||||
3. **E2E Validation** (after deployment):
|
||||
- Run a Claude Code session with the env var set
|
||||
- Query BigQuery `anthropic.api_production.api_usage` to verify `metadata_user_id` is populated
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Internal Only**: The tree-shaking ensures this feature is not available in public builds
|
||||
2. **No Secrets**: The metadata should not contain sensitive information
|
||||
3. **Validation**: Input validation prevents injection attacks
|
||||
|
||||
## Related Files to Modify (Internal Repo)
|
||||
|
||||
Based on patterns in the bundled CLI, likely files to modify:
|
||||
|
||||
1. `src/config/env.ts` or similar - Add env var reading
|
||||
2. `src/api/anthropic.ts` or similar - Add metadata to API requests
|
||||
3. `build.config.ts` or similar - Add build-time flag for internal builds
|
||||
4. Tests for the above
|
||||
|
||||
## References
|
||||
|
||||
- Slack thread: Internal API metadata discussion
|
||||
- BigQuery tables: `anthropic.api_production.api_usage`, `api_events`
|
||||
- Anthropic API docs: [Messages API metadata parameter](https://docs.anthropic.com/en/api/messages)
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,32 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 2.0.69
|
||||
|
||||
- Minor bugfixes
|
||||
|
||||
## 2.0.68
|
||||
|
||||
- Fixed IME (Input Method Editor) support for languages like Chinese, Japanese, and Korean by correctly positioning the composition window at the cursor
|
||||
- Fixed a bug where disallowed MCP tools were visible to the model
|
||||
- Fixed an issue where steering messages could be lost while a subagent is working
|
||||
- Fixed Option+Arrow word navigation treating entire CJK (Chinese, Japanese, Korean) text sequences as a single word instead of navigating by word boundaries
|
||||
- Improved plan mode exit UX: show simplified yes/no dialog when exiting with empty or missing plan instead of throwing an error
|
||||
- Add support for enterprise managed settings. Contact your Anthropic account team to enable this feature.
|
||||
|
||||
## 2.0.67
|
||||
|
||||
- Thinking mode is now enabled by default for Opus 4.5
|
||||
- Thinking mode configuration has moved to /config
|
||||
- Added search functionality to `/permissions` command with `/` keyboard shortcut for filtering rules by tool name
|
||||
- Show reason why autoupdater is disabled in `/doctor`
|
||||
- Fixed false "Another process is currently updating Claude" error when running `claude update` while another instance is already on the latest version
|
||||
- Fixed MCP servers from `.mcp.json` being stuck in pending state when running in non-interactive mode (`-p` flag or piped input)
|
||||
- Fixed scroll position resetting after deleting a permission rule in `/permissions`
|
||||
- Fixed word deletion (opt+delete) and word navigation (opt+arrow) not working correctly with non-Latin text such as Cyrillic, Greek, Arabic, Hebrew, Thai, and Chinese
|
||||
- Fixed `claude install --force` not bypassing stale lock files
|
||||
- Fixed consecutive @~/ file references in CLAUDE.md being incorrectly parsed due to markdown strikethrough interference
|
||||
- Windows: Fixed plugin MCP servers failing due to colons in log directory paths
|
||||
|
||||
## 2.0.65
|
||||
|
||||
- Added ability to switch models while writing a prompt using alt+p (linux, windows), option+p (macos).
|
||||
|
||||
@@ -93,6 +93,11 @@ Found 3 issues:
|
||||
|
||||
<link to file and line with full sha1 + line range for context>
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
|
||||
<sub>- If this code review was useful, please react with 👍. Otherwise, react with 👎.</sub>
|
||||
|
||||
---
|
||||
|
||||
- Or, if you found no issues:
|
||||
@@ -103,6 +108,8 @@ Found 3 issues:
|
||||
|
||||
No issues found. Checked for bugs and CLAUDE.md compliance.
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
|
||||
---
|
||||
|
||||
- When linking to code, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-code/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
@@ -15,7 +14,6 @@
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
@@ -27,7 +25,6 @@
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
@@ -39,7 +36,6 @@
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -40,27 +40,7 @@ echo ""
|
||||
echo "Checking root structure..."
|
||||
VALID_EVENTS=("PreToolUse" "PostToolUse" "UserPromptSubmit" "Stop" "SubagentStop" "SessionStart" "SessionEnd" "PreCompact" "Notification")
|
||||
|
||||
# Detect format: plugin format has { description?, hooks: {...} } wrapper
|
||||
# Settings format has events directly at root level
|
||||
is_plugin_format=false
|
||||
if jq -e '.hooks' "$HOOKS_FILE" >/dev/null 2>&1; then
|
||||
is_plugin_format=true
|
||||
HOOKS_PATH=".hooks"
|
||||
echo "Detected plugin format (with 'hooks' wrapper)"
|
||||
|
||||
# Validate allowed root keys for plugin format
|
||||
for key in $(jq -r 'keys[]' "$HOOKS_FILE"); do
|
||||
if [ "$key" != "hooks" ] && [ "$key" != "description" ]; then
|
||||
echo "⚠️ Unknown root key in plugin format: $key (expected: 'hooks', 'description')"
|
||||
fi
|
||||
done
|
||||
else
|
||||
HOOKS_PATH="."
|
||||
echo "Detected settings format (events at root)"
|
||||
fi
|
||||
|
||||
# Validate event types
|
||||
for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
|
||||
found=false
|
||||
for valid_event in "${VALID_EVENTS[@]}"; do
|
||||
if [ "$event" = "$valid_event" ]; then
|
||||
@@ -82,12 +62,12 @@ echo "Validating individual hooks..."
|
||||
error_count=0
|
||||
warning_count=0
|
||||
|
||||
for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
hook_count=$(jq -r "$HOOKS_PATH.\"$event\" | length" "$HOOKS_FILE")
|
||||
for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do
|
||||
hook_count=$(jq -r ".\"$event\" | length" "$HOOKS_FILE")
|
||||
|
||||
for ((i=0; i<hook_count; i++)); do
|
||||
# Check matcher exists
|
||||
matcher=$(jq -r "$HOOKS_PATH.\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
|
||||
matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE")
|
||||
if [ -z "$matcher" ]; then
|
||||
echo "❌ $event[$i]: Missing 'matcher' field"
|
||||
((error_count++))
|
||||
@@ -95,7 +75,7 @@ for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
fi
|
||||
|
||||
# Check hooks array exists
|
||||
hooks=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
|
||||
hooks=$(jq -r ".\"$event\"[$i].hooks // empty" "$HOOKS_FILE")
|
||||
if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then
|
||||
echo "❌ $event[$i]: Missing 'hooks' array"
|
||||
((error_count++))
|
||||
@@ -103,10 +83,10 @@ for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
fi
|
||||
|
||||
# Validate each hook in the array
|
||||
hook_array_count=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks | length" "$HOOKS_FILE")
|
||||
hook_array_count=$(jq -r ".\"$event\"[$i].hooks | length" "$HOOKS_FILE")
|
||||
|
||||
for ((j=0; j<hook_array_count; j++)); do
|
||||
hook_type=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
|
||||
hook_type=$(jq -r ".\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE")
|
||||
|
||||
if [ -z "$hook_type" ]; then
|
||||
echo "❌ $event[$i].hooks[$j]: Missing 'type' field"
|
||||
@@ -122,7 +102,7 @@ for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
|
||||
# Check type-specific fields
|
||||
if [ "$hook_type" = "command" ]; then
|
||||
command=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
|
||||
command=$(jq -r ".\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE")
|
||||
if [ -z "$command" ]; then
|
||||
echo "❌ $event[$i].hooks[$j]: Command hooks must have 'command' field"
|
||||
((error_count++))
|
||||
@@ -134,7 +114,7 @@ for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
fi
|
||||
fi
|
||||
elif [ "$hook_type" = "prompt" ]; then
|
||||
prompt=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
|
||||
prompt=$(jq -r ".\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE")
|
||||
if [ -z "$prompt" ]; then
|
||||
echo "❌ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field"
|
||||
((error_count++))
|
||||
@@ -148,7 +128,7 @@ for event in $(jq -r "$HOOKS_PATH | keys[]" "$HOOKS_FILE"); do
|
||||
fi
|
||||
|
||||
# Check timeout
|
||||
timeout=$(jq -r "$HOOKS_PATH.\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
|
||||
timeout=$(jq -r ".\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE")
|
||||
if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then
|
||||
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
|
||||
echo "❌ $event[$i].hooks[$j]: Timeout must be a number"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"matcher": "*",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Validate all hooks.json files in the repository
|
||||
# This script can be run in CI to ensure all plugins have valid hook configurations
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
VALIDATOR="$REPO_ROOT/plugins/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh"
|
||||
|
||||
echo "🔍 Validating all hooks.json files in the repository..."
|
||||
echo ""
|
||||
|
||||
# Find all hooks.json files
|
||||
mapfile -t HOOKS_FILES < <(find "$REPO_ROOT/plugins" -name "hooks.json" -type f 2>/dev/null)
|
||||
|
||||
if [ ${#HOOKS_FILES[@]} -eq 0 ]; then
|
||||
echo "No hooks.json files found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${#HOOKS_FILES[@]} hooks.json file(s)"
|
||||
echo ""
|
||||
|
||||
errors=0
|
||||
for hooks_file in "${HOOKS_FILES[@]}"; do
|
||||
relative_path="${hooks_file#$REPO_ROOT/}"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo "📄 $relative_path"
|
||||
echo ""
|
||||
|
||||
if bash "$VALIDATOR" "$hooks_file"; then
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
((errors++))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
|
||||
if [ $errors -eq 0 ]; then
|
||||
echo "✅ All ${#HOOKS_FILES[@]} hooks.json file(s) are valid!"
|
||||
exit 0
|
||||
else
|
||||
echo "❌ $errors hooks.json file(s) have validation errors"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user