Skip to content

Attribute requests by repository, branch and user - #25

Open
Thibaultjaigu wants to merge 8 commits into
mainfrom
feat/request-attribution-headers
Open

Attribute requests by repository, branch and user#25
Thibaultjaigu wants to merge 8 commits into
mainfrom
feat/request-attribution-headers

Conversation

@Thibaultjaigu

Copy link
Copy Markdown
Contributor

Generalises the Claude-Code-only curl -fsSL https://www.requesty.ai/claude/install.sh | bash
header-injection wrapper into native, per-harness request attribution in the CLI, so repo, branch
and user cost attribution works for every integration we support rather than one.

What it does

The wizard gains a step between model and merge/overwrite asking what requests are attributed to.
Opting in sends three headers, which Requesty turns into extra.Repo, extra.Branch and
extra.User dimensions and strips before calling a provider:

Header Value
X-Requesty-Repo origin remote, without scheme or .git suffix
X-Requesty-Branch branch checked out where the harness runs
X-Requesty-User username on this machine

Off unless asked for. Repo and branch names are often private and branch names are unbounded
cardinality, so the step starts on Keep requests unattributed.

The hard part: setup time vs request time

The CLI runs once, from one directory. Harnesses run later, from anywhere. A repo or branch baked
into a config file at setup time is not stale, it is confidently wrong. So each harness resolves
them the only way it can:

Harness How Shell hook
Pi runs the git command itself via !cmd not needed
Codex env_http_headers names variables yes
OpenCode {env:VAR} placeholders yes
Claude Code ANTHROPIC_CUSTOM_HEADERS yes, only source
DeepSeek Harness fixed values only, so user only n/a
Hermes ignores config headers in anthropic_messages mode impossible

One managed hook covers all three env readers: ~/.requesty/shell/attribution.{sh,fish} plus a
marker-delimited source line in .zshrc/.bashrc/config.fish. It refreshes before each prompt,
so a git checkout is picked up without a new shell. No wrapper functions, no per-harness shims —
users keep typing claude and codex.

Two things worth a reviewer's attention

  • Claude Code cannot be un-attributed from the wizard. It has no headers of its own to remove,
    and one hook serves every harness, so opting out for one harness must not delete it. Removal is
    manual and documented in the README. A requesty attribution off command is the real answer if
    we want one.
  • extra.Branch is unbounded cardinality. Every feature/… branch anyone works on becomes a
    distinct dimension value. Worth checking with whoever owns the analytics store before this gets
    popular.

Also in here

  • internal/fileio extracted from harnesses, so the hook writer reuses the same atomic write,
    one-time .requesty.bak backup and 0600 behaviour.
  • Merge learned to remove. mergePatch only ever set values, so attribution headers written by
    an opted-in run survived opting back out — a privacy feature you could not turn off without
    overwrite mode. A removal sentinel fixes it: headerPatch now sets what is asked for and removes
    every attribution header that is not.
  • Dropped dead Hermes config. default_headers is a documented no-op in anthropic_messages
    mode, and the X-Origin-Title it set is not a header the router recognises (only X-Title is).

Verification

gofmt -l . silent, go build ./..., go vet ./..., go test ./... all pass. New tests cover
every harness through both the merge and overwrite paths, hand-added headers surviving both a write
and a removal, the wizard flow in each direction, and the generated hook sourced under a real sh.

🤖 Generated with Claude Code

Each harness now has a single headers method feeding both the merge and
overwrite paths, spelling the dynamic dimensions the way that harness
resolves them: Codex reads environment variables, OpenCode expands
{env:...} placeholders, and Pi runs the shell command itself. The
DeepSeek Harness can only be given fixed values, so it carries the user
and leaves the repository and branch out rather than freezing them to
wherever the CLI happened to run.
Hermes only applies the default_headers from its config file when it
speaks the OpenAI format. The Requesty integration uses the native
Anthropic Messages format, so the block was never sent, and the title it
named was X-Origin-Title, which is not a header Requesty reads.
The step sits between the model and the configuration mode, and starts on
keeping requests unattributed: a repository name, a branch name and a
username only leave the machine when the user asks for it. Choosing to
attribute installs the shell hook the harnesses that read their
environment depend on, and the list reports which startup file it went
into, as the exported variables only reach a shell started afterwards.
A merge only ever set values, so headers written by an opted-in run stayed
in a harness config after the user opted out again: a privacy feature that
could not be turned off without overwriting the file.

The merge now understands a removal patch value, and headerPatch marks every
attribution header the current set does not carry, so configuring again sets
what is asked for and removes the rest.
Comment thread internal/attribution/shell.go
Comment thread internal/fileio/fileio.go
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.

1 participant