assisted-by
v0.4.3
Published
Attach coding-agent sessions (Claude Code, Codex, OpenCode) to git commits and pull requests, redacted.
Readme
assisted-by
Attach coding-agent sessions (Claude Code, Codex, OpenCode) to git commits and pull requests.
A commit made while an agent session is active gets an Assisted-by: <trace id> trailer: a random id that says nothing about the session. The commit also records where each session stood when it was made. The sessions are synced, redacted, to tracehub, and the GitHub App puts a small icon into the corner of the pull request's description that links to them.
SPEC.md describes the whole system in detail.
Install
Run the guided setup in your terminal:
npx --yes [email protected] setup ~/GitHubsetup reuses your saved GitHub sign-in or opens the browser sign-in flow. It searches your local checkouts and offers a searchable multi-select of accessible assistant-ui repositories where the GitHub App is installed. Review how conversations and pushed changes are shared with maintainers, then confirm automatic trace uploads. Tools stay owner-only. Global Git hooks are a separate choice, defaulting to no. Cancelling or declining sharing leaves hooks and consent unchanged.
If you omit the directory, setup asks where your repositories are, with your current directory as the default. Setup finds its local repositories, direct child repositories, and their registered Git worktrees; it does not scan your whole machine. It configures both Git hooks and saves consent for selected repositories. Existing POSIX shell hooks and Husky user hooks are preserved, including arguments, pre-push input, and their exit status. Shared external hook directories and unsupported hook scripts require manual setup. Both hook arguments are forwarded with "$@".
Setup selects local checkouts only. Future clones and new repositories are not automatically configured unless you choose global hooks; upload consent still applies only to the repositories you selected. Run setup again after cloning or to update your hooks. Existing consent and unchanged managed hooks are preserved on repeat runs. The standalone enable command also allows saving consent for a repository not yet cloned. Managed hooks run npx --yes assisted-by@<exact version> pinned to the CLI version that configured them; a global npm installation is optional. Run setup again with a newer pinned version to update them.
Optional global Git hooks
To apply hooks to future clones and worktrees without configuring each checkout:
npx --yes [email protected] hooks --global
npx --yes [email protected] consent --yes --org assistant-uiInstalling global hooks does not grant consent. The dispatcher forwards all standard Git hooks to your previous global or inherited system hooks directory, or each repository's default hooks directory when neither was configured. Existing hook files, arguments, pre-push input, failure status, and warnings are preserved. The assisted-by hook itself stays nonblocking.
Repository, worktree, and command-specific core.hooksPath settings take precedence over the global dispatcher. Husky checkouts typically have such a setting: run setup inside those checkouts. The global dispatcher covers standard hook names in current Git; custom hooks introduced by later Git versions may require an updated CLI.
To restore your previous global hooks setting, without changing repository-specific configuration or consent:
npx --yes [email protected] hooks --global --removeSetup and removal refuse conflicting files or a global setting changed by another tool instead of overwriting them. Global setup also refuses global or system include/includeIf configuration, whose hook paths can vary across checkouts; use per-repository enable or manual setup in that case.
For manual setup with Husky:
npm i -D assisted-by husky
npx husky init
echo 'npx assisted-by hook prepare-commit-msg "$@"' > .husky/prepare-commit-msg
echo 'npx assisted-by hook pre-push "$@"' > .husky/pre-push
assisted-by consent --yes| hook | does |
| --- | --- |
| prepare-commit-msg | adds the trailer and records a marker per session |
| pre-push | uploads sessions and markers for pushed commits (15 s budget) |
- A hook command always exits 0. It never blocks a commit or a push, and never prompts.
- A hook command does nothing without consent, with
ASSISTED_BY=0, or outside a repository.
Upload scope
Uploads are restricted to repositories verified as owned by the GitHub organization assistant-ui (CLI 0.4.0 and later).
- All effective fetch and push remote URLs must name the same repository on
github.com, with the exact ownerassistant-ui(case insensitive). HTTPS, GitHub SSH URLs and[email protected]:…are accepted. Missing, unknown or conflicting remotes block uploads. A personal fork with an organization upstream is blocked. Duplicate remotes to the same repository are allowed. - Git's URL rewrites are resolved, and pre-push also checks the destination URL Git passes to the hook. Pre-push scripts must forward
"$@"and Git's stdin ref updates; missing arguments or ref input block upload, even if the configured origin is eligible. Usesyncfor manual uploads. The commit hook skips trace markers for ineligible remotes; it makes no network request. - Before sending any trace or marker, the upload run asks GitHub for the canonical repository owner using the signed-in user's token. The returned owner must be the
assistant-uiorganization and the canonical repository name must match the remotes. Redirects, transfers, renames, missing fields, access failures and network failures block uploads. Update remotes after a rename. An organization-owned fork remains eligible; a fork owned elsewhere does not. - This applies to both
assisted-by syncand the pre-push hook, including previously pending markers. Upload methods also verify ownership themselves. A refused hook still exits 0, preserving pending work for inspection and retry; manual sync reports failure. - Consent and sign-in remain required. This change does not install hooks, grant access, change trace visibility, or send traces to GitHub.
statusshows local eligibility; the fresh GitHub check occurs at upload time.
See the rollout review for current privacy limitations, the proposed audience model, and a conditional announcement draft.
Sign in
Setup includes sign-in. Use this command to manage your account separately:
npx --yes [email protected] login- Runs the device flow of GitHub: displays the code on its own line, asks you to copy it and press Enter, then opens GitHub and waits for confirmation. Without an interactive terminal, it prints the code and URL for you to open manually.
- If already signed in, shows your account and asks whether to sign in again. Declining or cancelling keeps your current sign-in; a replacement is saved only after GitHub confirms the new account. Without an interactive terminal, an existing sign-in is kept.
- The credentials go to
~/.config/assisted-by/auth.json(mode0600). The access token is refreshed when needed; a refresh that GitHub refuses removes the file. logoutremoves the file.- The GitHub App
assisted-by-botmust be installed. Uploads require current push access and verifiedassistant-uiorganization ownership. Reading traces uses the audience policy below; maintainers have current GitHub write, maintain or admin access to the repository. - A hook that finds no sign-in leaves its work pending and says so.
| setting | environment | default |
| --- | --- | --- |
| tracehub | ASSISTED_BY_TRACEHUB | https://assisted-by.aui.dev |
| client id of the GitHub App | ASSISTED_BY_GITHUB_CLIENT_ID | Iv23limQrRDTbi0xJG3f |
Consent
Nothing is attached until you grant consent for a repository, organization, or all GitHub repositories:
npx --yes [email protected] consent --yes # current origin
npx --yes [email protected] consent --yes --repo assistant-ui/demo # any checkout, or none
npx --yes [email protected] consent --yes --org assistant-ui # includes future repos
npx --yes [email protected] consent --yes --all # all GitHub origins
npx --yes [email protected] consent --no --repo assistant-ui/privateSaying yes means: the agent sessions active in this repository are uploaded, redacted, to Tracehub. New sessions share conversations and pushed changes with repository maintainers with GitHub write, maintain or admin access; tools stay owner-only and everyone else sees nothing. Sharing covers the whole session, including future uploads. On the way in, tracehub sends the redacted lines to TypeSafe's Jev model to find what the patterns missed (see Privacy).
Answers are kept in ~/.config/assisted-by/consent.json under normalized GitHub origin keys: github.com/owner/repo, github.com/owner/*, or github.com/*. Every clone and worktree shares the answer; filesystem paths are never consent identities. The most specific recorded answer wins: repository, then organization, then all. An explicit repository refusal overrides broader grants; an explicit repository grant overrides an organization refusal.
Consent remains separate from upload eligibility and hook installation. Even --all does not authorize non-GitHub origins or bypass the current assistant-ui-only ownership policy, GitHub App installation, repository access, or sign-in checks. Broader consent may cover future eligible repositories, but this release only uploads verified assistant-ui repositories. It does not make traces public or alter existing sharing policies.
| consent | a commit with an active session |
| --- | --- |
| yes | gets the trailer and markers |
| no | is left alone |
| not given | is left alone, with a hint to run assisted-by consent --yes or --no |
How it works
- Commit.
prepare-commit-msgfinds the sessions of the commit (see Discovery). If the message has noAssisted-bytrailer yet, it adds one with a new 16-hex trace id. For each session it appends a marker to.git/assisted-by/markers.jsonl:{traceId, session, line, ts, uploaded}, wherelineis the number of complete lines the session file had. When a tool is still executing the Git commit, the local marker also retains itspendingCallId. Merge and squash commits are skipped. - Amend, rebase, squash. The message keeps its trailer, so the commit keeps its trace id. A new marker for the same trace id and session replaces the old one.
- Push.
pre-pushreads Git's ref updates and syncs only sessions attached to commits being pushed. New branches exclude commits already known on that remote. Deletions upload nothing; an unavailable remote base leaves traces pending with a recovery warning. Each completed session uploads its pending markers immediately, recording each success before starting another session. Before uploading, it extends a commit cut through that commit tool's matching result if the agent has written it, updating both its line and timestamp. Later work stays outside the cut. When commit and push run inside one tool call, the initial cut uploads provisionally;assisted-by sync, or a later push containing that commit, refines it after the result arrives. The call identity stays local and the trace id stays unchanged. Manual sync also brings previously marked sessions up to date. A session file that did not change since its last sync is skipped. - Pull request. The GitHub App receives the
pull_requestevent, reads the trace ids from the commit messages, finds the sessions marked under them, and writes a link at the start of the description, between<!-- assisted-by:start -->and<!-- assisted-by:end -->: a small icon in the top right corner. The icon is one image for every pull request, so it is there at once, including when no sessions have been uploaded yet. A link to a Claude Code session (https://claude.ai/code/session_…on a line of its own) is taken out of the description: the icon takes its place. - Reading. The icon links to
/pr/<owner>/<repo>/<number>, which lists the sessions. A pull request of one session opens that session right away. Each session is at/t/<owner>/<repo>/<trace id>/<agent>-<id>.jsonl, through the final reply of the commit’s current turn, with the next user prompt and later work folded under it;/s/<owner>/<repo>/<name>shows it whole. The home page lists your pull requests that have sessions, and the pages this browser showed last. - Without a pull request. A push prints a link per commit it marked,
/t/<owner>/<repo>/<trace id>: the trace, which is the sessions attached to that commit.
Sync
A session is sent line by line, in chunks of 1 MB. Each request names the point it continues from (the line count and a hash of the lines before it) and tracehub appends only when it is at that point. How far each session got is kept in .git/assisted-by/sessions/<name>.json.
- When the agent rewrote its transcript, the session is sent again from the start and replaces the one on tracehub.
- When tracehub is at another point than this machine remembers, the sync continues from tracehub's point if the file agrees with it, and from the start otherwise.
- Every request can be repeated. A failed hook prints a warning and recovery command on Git's stderr while letting the push continue. Completed markers stay saved; run
assisted-by syncto retry all pending traces without the shared hook time budget, including commits already pushed.
Discovery
The sessions of a commit, by precedence:
ASSISTED_BY_ID=<claude|codex|opencode>:<id>in the environment (comma-separated for several).- Recently active sessions with an open agent tool call executing
git commit. This uses the existing commit-call identity and preserves several sessions when several commit calls are active. - If no committing session is found, the sessions of the repository written to since the last commit (the last 24 hours in a repository without a commit). This fallback preserves sessions for commits made manually.
Where the sessions are:
- Claude Code:
~/.claude/projects/<encoded cwd>/<session id>.jsonl. The cwd is encoded by replacing every character outside[A-Za-z0-9-]with-. Subagents are in<session id>/subagents/agent-<id>.jsonl, with an optionalagent-<id>.meta.json. - Codex:
~/.codex/sessions/YYYY/MM/DD/rollout-<time>-<thread id>.jsonl. The first line (session_meta) has thecwd, which has to be inside the repository. A sub-thread names its parent inparent_thread_id. - OpenCode: the database
~/.local/share/opencode/opencode.db. A session has thedirectoryit was started in, which has to be inside the repository. The session a task started names its parent inparent_id. The CLI reads the database withnode:sqlite, so it needs Node 22.13 or newer.
Subagents are synced with their parent and shown under the tool call that launched them.
Commands
| command | does |
| --- | --- |
| setup [directory] | guided GitHub sign-in, local repository selection, sharing consent, and Git hooks |
| login, logout | sign in and out; available separately for scripts |
| enable [directory] | search and select installed assistant-ui repositories, save consent, and configure local Git hooks |
| consent --yes\|--no [--repo <owner/repo> \| --org <owner> \| --all] | records upload consent by normalized GitHub origin scope |
| hooks --global [--remove] | installs or removes optional global hook dispatchers without granting consent |
| status | repository, consent, tracehub, sign-in, markers, next commit's sessions |
| sync | retries all locally marked sessions and pending markers without the shared hook time budget |
| scan <session.jsonl\|agent:id\|commit> | what a sync would mask: types and counts, never values; exits 1 on a finding |
| deny add <literal>, deny list | the literals that are always redacted |
| hook <prepare-commit-msg\|pre-push> | what the git hooks call |
Privacy
What leaves the machine, for each line of a session:
- Thinking and reasoning are replaced by
[omitted]. - Tool output is truncated to 4096 characters.
- Every string is run through the detectors below; a match becomes
[redacted]. - The home directory becomes
~.
The agent's own file is never changed. The pre-push hook reports what it masked on stderr: assisted-by: claude:<id>: masked 3 findings (high_entropy ×2, long_hex ×1) before upload.
| detector | matches |
| --- | --- |
| deny_list | a literal of the deny list |
| url_credentials | the password in scheme://user:password@host |
| pem_block | -----BEGIN …----- to its end, or to the end of the text |
| openai_key, stripe_key, webhook_secret, github_token, gitlab_token, aws_key, slack_token, google_api_key, neon_key, pat_token, square_token, dotted_token | the token formats of those services, by prefix |
| jwt | eyJ….….… |
| authorization_header, bearer_token, basic_auth | what follows Authorization:, Bearer, Basic |
| keyed_assignment | the value of <name>_key=, token:, password=, SECRET= and the like |
| long_hex | 64 or more hex characters |
| keyed_hex | 32 to 63 hex characters with token, secret, key, password, bearer, auth or api in the 40 characters before them; a bare git sha is left alone |
| high_entropy | 32 or more characters of [A-Za-z0-9_\-+/=.] with two digits or more, two character classes or more, and an entropy above 3.8 bits per character; not hex, a UUID, a hostname or a path |
What tracehub does with a line
- It runs the detectors once more.
- It asks Jev, TypeSafe's decision model, whether the redacted line holds a secret or personal data that the patterns cannot see: a password in a sentence, a person's address, a company secret.
- A line Jev finds sensitive is stored empty, so the lines after it keep their number. The hook says how many:
assisted-by: claude:<id>: tracehub withheld 2 line(s) that look sensitive. - When Jev cannot be reached, tracehub stores nothing and answers 503; the next push tries again.
Lines are asked in windows of 4000 characters; a window that is sensitive is asked again line by line. The redacted lines leave tracehub for TypeSafe's API. Without TYPESAFE_API_KEY tracehub skips steps 2 to 4.
Deny list
assisted-by deny add <literal> adds a literal of 6 characters or more to ~/.config/assisted-by/deny.txt (mode 0600). The file holds the literals themselves, so treat it like the secrets. deny list prints them masked.
Tracehub
tracehub/ is a Cloudflare Worker over an R2 bucket; tracehub/README.md says how to deploy it. GitHub signs the users in and says who may push to a repository.
| route | does |
| --- | --- |
| PUT /repos/:owner/:repo/sessions/:name | appends lines to a session |
| PUT /repos/:owner/:repo/traces/:id/markers/:name | marks where a session stood at a commit |
| GET / | your pull requests that have sessions; recently viewed pages |
| GET /t/:owner/:repo/:trace | a trace: the sessions attached to a commit |
| GET /t/:owner/:repo/:trace/:name | a session as the trace saw it |
| GET /s/:owner/:repo/:name | the session whole |
| POST /s/:owner/:repo/:name/visibility | makes a session public or private; for who uploaded it |
| GET /pr/:owner/:repo/:number | the sessions of a pull request |
| GET /icon.svg | the icon in the pull requests, public |
| GET /login, /logout, /api/auth/callback | browser sign-in |
| POST /github/webhook | the GitHub App's webhook |
| GET /healthz | {ok: true} |
A session page is headed by the title Claude Code gave the session. Its sidebar has the numbers of the session, who sees it, and the prompts. There are two themes, Chat and Claude Code (the look of the terminal); the browser keeps the choice.
A new session defaults to Conversation + changes for repository maintainers with GitHub write, maintain or admin access, Everything for its uploader, and Nothing for everyone else. Only the uploader changes the maintainer/everyone audience levels on its page. Existing private sessions remain owner-only until explicitly shared.
The two PUT routes take Authorization: Bearer <access token of the GitHub user>. The pages take the cookie of the browser sign-in and send a signed-out browser to /login, unless what they show is public.
GitHub App (once)
The App of the deployed tracehub exists; tracehub reads its key from the bucket, _config/github-app.json ({appId, pem, webhookSecret}).
For another deployment, create an App in the GitHub organization:
- Permissions: pull requests: write, contents: read, metadata: read. Event: pull request. Webhook
<tracehub>/github/webhook. - Callback URL
<tracehub>/api/auth/callback, for the browser sign-in. - Device flow enabled, for
assisted-by login. - A client secret, for the browser sign-in.
Install it on the repositories, set its client id as GITHUB_CLIENT_ID in tracehub/wrangler.jsonc and as ASSISTED_BY_GITHUB_CLIENT_ID for the CLI, and store its values as secrets; the three of the key win over the bucket:
cd tracehub
npx wrangler secret put GITHUB_CLIENT_SECRET
printf %s "<app id>" | npx wrangler secret put GITHUB_APP_ID
npx wrangler secret put GITHUB_APP_PRIVATE_KEY < <app>.private-key.pem
printf %s "<webhook secret>" | npx wrangler secret put GITHUB_WEBHOOK_SECRETTrying it end to end
Do not try it with the session you are working in: it holds your real prompts and tool output, and what is uploaded stays in the bucket. Use a throwaway session and force it:
gh repo create you/assisted-by-playground --private --clone && cd assisted-by-playground
npm init -y && npm i -D assisted-by husky
npx husky init
echo 'npx assisted-by hook prepare-commit-msg "$@"' > .husky/prepare-commit-msg
echo 'npx assisted-by hook pre-push "$@"' > .husky/pre-push
npx --yes [email protected] setup .
# open a fresh agent session in this directory, ask it for something harmless
npx assisted-by status # names the sessions the next commit would attach
export ASSISTED_BY_ID=claude:<throwaway session id>
git commit --allow-empty -m "trial"
npx assisted-by scan HEAD # what the sync would mask
git push -u origin main # syncs the session, uploads the marker
unset ASSISTED_BY_IDWhy a random trailer
- A sha changes under amend, rebase and squash-merge, and a squash-merge happens on GitHub, where no hook runs.
- A session id is stable, but it would publish which session each contributor ran, and one session spans several commits.
- A random id is stable, says nothing, and git carries it along in the message.
Develop
npm run format:fix && npm run typecheck && npm test
cd tracehub && npm testTests are in test/ and tracehub/test/, fixtures in fixtures/. The end-to-end tests run the built CLI against the real tracehub app over an in-memory bucket (test/support/fakeTracehub.ts); nothing in the tests reads a real session.
A release is a tag: set the version in package.json, then git tag v<version> && git push --tags. .github/workflows/npm-publish.yaml publishes it to npm as the trusted publisher of the package.
Session audiences and data handling
New sessions default to owner Everything, maintainers Conversation + changes, and everyone else Nothing. Maintainers have current GitHub write, maintain or admin access to the exact repository, including outside collaborators. Organization membership alone and read/triage access do not qualify. Tracehub verifies the canonical repository name and permissions.push with the viewer's GitHub token on every audience check; positive maintainer results are not cached. Lookup errors or incomplete evidence fail closed. Only the uploader can choose Everything / Conversation + changes / Nothing for each audience. Owner access always wins; a verified maintainer uses the maintainer setting even when the public setting is broader. Unknown repository permissions receive the more restrictive setting.
Conversation + changes includes user prompts, assistant responses and pushed PR code diffs. Tool calls/results, subagents, local path metadata and tool metrics are removed on the server from HTML, structured page responses and live reviews. Changes require current GitHub repository access independently of session sharing. For a PR with multiple sessions, every attached session must permit its full diff; one public session cannot disclose another private session's changes.
Sharing is predictable and covers the whole session, including future uploads. Commit/PR anchors aid navigation; they do not determine permissions. Use a separate session for private work. Existing sessions are not automatically opted in: legacy private sessions become owner-only, legacy explicit public sessions retain public conversation access with tools removed, and explicit audience settings survive uploads.
| Destination | Sent | Omitted / protected |
| --- | --- | --- |
| GitHub | Ordinary pushed commits/code, random Assisted-by trace ID trailer, bot PR-link block, authentication/access requests | The uploader does not send transcripts to GitHub; GitHub still contains the actual committed code, including secrets accidentally committed there |
| Tracehub storage | Redacted parent/subagent transcript JSONL, session summaries, sync hashes, commit/session markers, audience settings | Thinking blocks are omitted by the CLI; detected secrets are masked and sensitivity-flagged lines withheld |
| Maintainers by default | Prompts, responses, permitted pushed PR diffs | Intermediate tools, subagent content, local path metadata and usage/tool metadata are server-excluded |
| Diff cache / Changes view | Filtered GitHub patches, including sanitized additions/deletions/context | .env and .env.* in every directory, credential directories/configs and private-key files; both sides of renames checked; no raw-content URL bypass |
| TypeSafe sensitivity service, when configured | Already pattern-redacted transcript chunks, including tool records | This is processing separate from maintainer visibility; its retention/access terms need operational review |
Secret detection is best effort, not a guarantee. Never put credentials into a shared session; masking cannot protect every unknown format or personal detail. Stored sessions have no automatic retention/deletion UI yet. Audience changes prevent future reads; they cannot retract material already downloaded.
Common absolute home prefixes in shared text/titles/diffs (for example /Users/alice, /home/alice and C:\Users\alice) are anonymized to ~ on server reads. Relative paths and path suffixes remain useful context; arbitrary local filenames and project names typed in the conversation are not promised to disappear. Redaction is best effort.
The persisted team audience now means repository maintainers rather than organization members with read access. Existing grants and denies retain their chosen levels, but apply to this new audience: outside write collaborators qualify and read-only organization members do not. Legacy private sessions stay owner-only, explicit public grants stay public, and uploads do not overwrite explicit choices. No bulk data migration or additional GitHub App permissions are required.
