sting
Query a GitHub or GitLab user's commits over a time window and hand them to an
LLM agent (or a terminal) in a consumable form.
sting is a single binary with subcommands:
sting init — guided first-time setup (strongly recommended).
sting auth — authenticate with GitHub or GitLab via OAuth (auth github, auth gitlab, auth status, auth logout). The verbose auth login github / auth login gitlab forms are also supported and behave identically.
sting mcp — runs an MCP server over stdio exposing two read-only tools,
get_commits and get_repo_activity.
sting <query flags> — prints a Markdown or JSON report locally.
sting activity — summarizes what happened in one GitHub repository over
a window, without naming an author.
sting install / uninstall / install list — register the MCP server
with your agent runtimes (Claude Code, Codex, OpenCode, Grok).
Configuration is resolved with viper (defaults < config file < env < flags),
so dedicated read-only PATs can live in sting's own config instead of relying on
ambient provider tokens.
Install
brew tap skaphos/tools https://github.com/skaphos/homebrew-tools
brew install --cask skaphos/tools/sting
Linux packages
.deb and .rpm packages for x86-64 and arm64 are attached to every
release:
sudo dpkg -i sting_<version>_amd64.deb # Debian, Ubuntu
sudo rpm -i sting-<version>-1.x86_64.rpm # Fedora, RHEL, openSUSE
There is no hosted apt or yum repository. Shipping a .deb is not the same
as running a package repository, and sting does not run one — hosting signed
repositories is a materially larger commitment
(DECISIONS/0001
puts it out of scope). Upgrading means downloading the next release's package.
Or install from source:
go install github.com/skaphos/sting/cmd/sting@latest
sting version reports the installed module version for this path — the binary reads the version
the Go toolchain records, so it is not limited to release builds.
Or build from this repo:
go -C tools tool task build # -> ./sting
A local build reports a pseudo-version and the revision it was built from, marked (modified) when
the working tree is dirty. A build made with -buildvcs=false records nothing, and sting version
says so rather than inventing a value.
Upgrading
sting does not update itself. Upgrade through the channel you installed from:
| Installed with |
Upgrade with |
| Homebrew |
brew upgrade --cask sting |
.rpm |
download the next release's .rpm, then sudo rpm -U |
.deb |
download the next release's .deb, then sudo dpkg -i |
go install |
go install github.com/skaphos/sting/cmd/sting@latest |
| container image |
docker pull ghcr.io/skaphos/sting:latest |
| downloaded archive |
download the next release's archive |
sting version reports what you are running, so you can tell whether an upgrade
is needed. Nothing in sting checks for updates in the background.
There is deliberately no sting update subcommand: verifying a release properly
in-process would more than double the binary for every user, and shipping a
self-updater that skips verification would be worse than shipping none. See
ADR 0011.
Getting started
sting init # guided setup (recommended)
sting auth github # or sting auth gitlab
sting query --author yourhandle --window 7d
Authentication (recommended)
The modern way to authenticate is with OAuth using sting init and the sting auth commands.
First-time setup
sting init
This is a guided wizard that:
- Defaults to GitHub (the primary/recommended provider)
- Can launch the OAuth flow for you
- Sets your default provider in
~/.config/sting/config.yaml
You can also be explicit:
sting init github # GitHub (default)
sting init gitlab # GitLab
Manual authentication
# GitHub (uses the public Skaphos OAuth app on github.com)
sting auth github
sting auth github --hostname ghe.example.com # GHES / bring-your-own app
# GitLab (device flow, same as `glab`)
sting auth gitlab
sting auth gitlab --hostname gitlab.example.com --client-id <YOUR_ID>
# The verbose `auth login <provider>` form is equivalent to `auth <provider>`:
sting auth login github
sting auth login gitlab
After authenticating you can check status or log out:
sting auth status
sting auth logout github
sting auth logout gitlab --hostname gitlab.example.com
Legacy PAT fallback
Personal Access Tokens are still fully supported as a fallback (especially useful
in CI or air-gapped environments). Note that a configured PAT (token/gitlab_token
in the config file, or STING_TOKEN/STING_GITLAB_TOKEN in the environment)
overrides any stored OAuth credential for that provider/host — it does not
merely fill a gap when OAuth is absent. Unset it if you want sting to use the
OAuth credential instead.
# config file
token: ghp_xxx
gitlab_token: glpat_xxx
# or environment
export STING_TOKEN=ghp_xxx
export STING_GITLAB_TOKEN=glpat_xxx
See docs/oauth-app-registration.md for how to
create the required OAuth applications and for important notes about trust,
governance, and when organizations should register their own apps instead of
using the public Skaphos ones.
sting auth --help also contains the current recommended patterns.
Agent integration (the main use case)
Register the MCP server with every detected runtime:
sting install # auto-detect Claude, Codex, OpenCode, Grok
sting install --claude # just one runtime
sting install --scope project # write project-scoped config in CWD
sting install --manual # print snippets instead of writing
sting install list # show registration state per runtime
sting uninstall # remove entries (prompts unless --yes)
Running the server from a container
If you would rather not install sting locally, MCP clients that support a
docker command can run the published image instead. It needs no Go toolchain
and works on x86-64 and arm64:
{
"mcpServers": {
"sting": {
"command": "docker",
"args": ["run", "--rm", "-i",
"-e", "STING_TOKEN",
"ghcr.io/skaphos/sting:latest"],
"env": { "STING_TOKEN": "ghp_xxx" }
}
}
}
The image runs as a non-root user, contains no credentials, and starts the MCP
server over stdio with no arguments. Credentials come from the environment,
using the same STING_ names as the local binary.
Registering the local binary
install writes a sting mcp entry pointing at the current executable. Because
get_commits is read-only (advertised via the MCP readOnlyHint
annotation), the Claude snippet also prints a paste-ready permissions.allow
block that auto-approves the tool — safe to accept without per-call prompts.
Then ask the agent naturally:
Give me all the commits of mfacenet in the last week and tell me what he's
working on.
The tool returns structured commit data plus a Markdown summary, so the
agent has clean material to describe the work.
CLI usage
# Last week of an author's commits, anywhere GitHub has indexed (Markdown).
sting --author mfacenet --window 7d
# JSON for piping into other tools.
sting --author mfacenet --window 7d -o json
# Within specific repos (most complete; works on private repos with a token).
sting --author mfacenet --scope repos --repos skaphos/sting,skaphos/other
# Across every repo in an org.
sting --author mfacenet --scope org --org skaphos --window 2w
# GitLab project commits.
sting --provider gitlab --author mfacenet --scope repos --repos skaphos/sting
# GitLab group commits, including projects in subgroups.
sting --provider gitlab --author mfacenet --scope org --org skaphos --window 2w
# Explicit bounds and per-commit line stats.
sting --author mfacenet --since 2026-05-01 --until 2026-05-15 --stats
# File-level evidence without full patches.
sting --author mfacenet --scope repos --repos skaphos/sting --window 7d --files
# Full bounded diffs for LLM analysis. This implies --files.
sting --author mfacenet --scope repos --repos skaphos/sting --window 7d --diffs --max-diff-bytes 60000
Run sting --help (or sting <command> --help) for the full flag list.
Queries return at most 100 commits by default to avoid routine rate-limit
pressure; pass --max-commits 0 only when you intentionally want an exhaustive
scan.
Every query is also bounded by a request ceiling (max_requests, default 500).
Reaching it returns the evidence gathered so far rather than aborting, so a
bounded run still exits 0.
Repository activity (sting activity)
sting query answers "what did this person do". sting activity answers
"what happened in this repository" — no author required:
# What happened in a repository over the last week.
sting activity --repo skaphos/sting --window 7d
# A specific branch and explicit bounds, as JSON.
sting activity --repo skaphos/sting --ref release/1.x \
--since 2026-07-01 --until 2026-07-08 -o json
# Check the cost before paying it. Gathers no evidence.
sting activity --repo skaphos/sting --window 30d --estimate
# Cap what a run may consume. A bounded run still exits 0.
sting activity --repo skaphos/sting --window 90d --max-requests 50
# Attribute changed paths to specific commits. Costs one request per commit.
sting activity --repo skaphos/sting --window 7d --enrich-commits 5
The output has four parts: the window's commits with full messages, the
aggregate per-file change set, the correlations between them, and a cost report.
It stays cheap by construction — the request count grows with commit pages,
not commit count, so a 250-commit window costs about five requests rather than
about 250.
What it does not cover
- It is a net comparison. The change set compares the window's start and end
states, so a file created and deleted inside the window, or edited and then
reverted, does not appear, and intermediate revisions are collapsed into one
net change. The commit list still shows that work; the change set does not.
- One reference at a time. It covers a single branch or tag. Work on other
branches, forks, or unmerged pull requests is not included.
--author narrows the commits, not the change set. A boundary comparison
has no notion of authorship, so the change set covers every author who touched
the reference in the window. The result says so explicitly.
- Attribution is labeled. Correlations are
observed only when per-commit
data was actually fetched (--enrich-commits); otherwise they are inferred
from a declared rule, and paths matching no rule are left unattributed rather
than guessed at.
- GitHub only.
--provider gitlab is rejected with a specific message;
GitLab commit queries via sting query are unaffected.
- Windows are bounded by committer date, which GitHub's API filters on. That
differs from author date after a rebase, cherry-pick, or amend; the result
records which basis was used.
Every one of these limits is also emitted as a disclosure in the output, so an
agent reading the result sees them rather than having to consult this document.
Evidence depth
The default query returns commit metadata and messages only. Use the evidence
flags when you want an agent to explain the actual code changes:
--stats adds per-commit additions, deletions, and total changed lines.
--files adds changed file paths, statuses, and per-file line counts (and also populates the per-commit totals).
--diffs adds patch text for each changed file and implies --files.
--max-diff-bytes caps patch text per commit; truncated files are marked in
JSON and Markdown.
--prs (GitHub, scope=repos/org) also discovers commits on open
pull-request branches. Unmerged work is not on a default branch, so commit
search and branch listing miss it; this enumerates open PRs per repo and
merges author-matching, in-window commits. Each result carries a source
field (search, repo, or pull/<n>) so the discovery origin is auditable,
and Markdown flags PR-branch commits with [pull/<n>]. It costs extra API
calls (one PR list plus one commit list per open PR), so it is off by default.
GitHub fetches this evidence from per-commit detail calls. GitLab uses
with_stats for line stats and commit diff calls for file evidence. Keep full
diffs explicit because they cost extra API calls and can be token-heavy for an
LLM context. The MCP get_commits tool therefore treats an omitted
include_diffs argument as false, even when the server config enables diffs;
callers must explicitly pass include_diffs: true to request patch text.
Scopes
| provider |
scope |
how it finds commits |
notes |
| GitHub |
search |
GitHub commit search by author: (or author-email: for emails) |
global (public-only) unless scoped; 1000-result cap |
| GitHub |
repos |
lists commits in each owner/repo you name, filtered by author |
most complete; supports private repos with a token |
| GitHub |
org |
enumerates an org's repos, then lists commits in each |
needs org read access for private repos |
| GitLab |
repos |
lists commits in each group/project or project ID |
supports nested group paths; GitLab search not used |
| GitLab |
org |
treats org as a GitLab group and includes subgroup projects |
needs group/project read access for private data |
GitLab search scope is not supported yet. GitLab's search API does not map
cleanly to sting's date-bounded author query contract, so use repos or org
with --provider gitlab.
In org scope sting enumerates the org's repos and then lists each repo's
commits. A repo that cannot be listed for a reason specific to that repo — an
empty repository (GitHub returns 409), or one that is gone or not visible to
the token (403/404/410/451) — is skipped rather than aborting the
whole scan, so one bad repo can no longer sink an entire org query. Skipped
repos are reported in the result: a skipped array in JSON (each entry has a
repo and a reason) and a Skipped line in Markdown. Global failures
(rate limits, auth, server errors) still abort, since continuing would only
retrip them on every remaining repo. The GitLab org scope skips unreadable
projects the same way. The explicit repos scope does not skip: a repo you
named yourself fails loudly so a typo or access gap is not silently dropped.
Searching private orgs
A bare search is a global author query, which GitHub limits to public
repos. To reach a private org via the search index, scope the query by
combining search with --org (or --repos):
# Adds `org:Alaska-Airlines-Shared` to the search query.
sting --author mfacenet --scope search --org Alaska-Airlines-Shared --window 7d
--author accepts either a GitHub login or a commit email. An email is
detected automatically and queried with the author-email: qualifier, since
GitHub's commit search does not match emails against author:. Note the two can
return different results: a login matches commits GitHub attributes to that
account, while an email matches the raw commit author regardless of account
linkage.
Two requirements for any private-org result:
- Token access — the PAT must have read access to the private repos. If the
org enforces SAML/SSO, the token must be SSO-authorized for it (classic
PAT) or approved with repo/contents read (fine-grained PAT); otherwise private
results are silently excluded.
- Indexing — commit search only covers default branches and skips commits
attributed to an email not linked to the GitHub account.
For the most complete private-org coverage (not limited by the search
index), prefer --scope org --org Alaska-Airlines-Shared, which lists commits
per repo directly. search --org is faster (one query vs. per-repo listings)
but subject to the indexing caveats above.
Time window
--window accepts 7d, 2w, 48h, 30m, etc. --since/--until accept
YYYY-MM-DD or RFC3339. --since overrides --window; --until defaults to now.
Configuration
Resolved in increasing precedence: built-in defaults → config file → environment
(STING_*) → flags. The config file is discovered as config.yaml under
$XDG_CONFIG_HOME/sting, ~/.config/sting, ~/.sting, or the current
directory, or pointed at explicitly with --config path.yaml.
| key |
env |
flag |
default |
meaning |
provider |
STING_PROVIDER |
(--provider) |
github |
provider when unspecified |
token |
STING_TOKEN |
--token |
— |
dedicated GitHub PAT |
base_url |
STING_BASE_URL |
--base-url |
github.com |
GitHub Enterprise API root |
gitlab_token |
STING_GITLAB_TOKEN |
--gitlab-token |
— |
dedicated GitLab PAT |
gitlab_base_url |
STING_GITLAB_BASE_URL |
--gitlab-base-url |
GitLab.com |
GitLab API v4 root |
per_page |
STING_PER_PAGE |
--per-page |
100 |
API page size (1–100) |
max_commits |
STING_MAX_COMMITS |
--max-commits |
100 |
cap on returned commits (0 = unlimited) |
default_scope |
STING_DEFAULT_SCOPE |
(--scope) |
search |
scope when unspecified |
default_window |
STING_DEFAULT_WINDOW |
(--window) |
7d |
look-back when since unspecified |
default_repos |
STING_DEFAULT_REPOS |
(--repos) |
— |
repo/project list for repos scope |
default_org |
STING_DEFAULT_ORG |
(--org) |
— |
org/group for org scope |
default_format |
STING_DEFAULT_FORMAT |
(-o) |
markdown |
CLI output format |
include_stats |
STING_INCLUDE_STATS |
(--stats) |
false |
fetch additions/deletions per commit |
include_files |
STING_INCLUDE_FILES |
(--files) |
false |
fetch changed file summaries |
include_diffs |
STING_INCLUDE_DIFFS |
(--diffs) |
false |
fetch bounded patch text |
max_diff_bytes |
STING_MAX_DIFF_BYTES |
(--max-diff-bytes) |
60000 |
per-commit patch byte cap |
include_prs |
STING_INCLUDE_PRS |
(--prs) |
false |
discover open-PR branch commits (GitHub) |
Keys in parentheses are per-query request flags that override the resolved
default for a single invocation. See config.example.yaml.
Development
task tidy vet test # tidy + vet + test
task test-race # tests under the race detector
task run -- --author octocat --scope repos --repos octocat/Hello-World --since 2008-01-01
Package and API reference (godoc):
go doc ./... # synopsis of every package
go doc ./ghclient Client # a specific type
go doc ./gitlabclient Client
Documentation
Layout
Public packages (importable; the evidence contract — see
ADR 0004):
model/ domain types (leaf) + Result SchemaVersion
config/ Config, viper keys, window/time parsing, query resolution
ghclient/ go-github wrapper + scope dispatch + normalization
gitlabclient/ GitLab REST wrapper + scope dispatch + normalization
Application layer (internal):
cmd/sting/ thin entrypoint -> internal/cli
internal/cli/ cobra command tree + viper wiring
internal/commitclient/ provider client selection
internal/render/ JSON + Markdown rendering
internal/mcpserver/ MCP server; read-only get_commits tool
internal/mcpinstall/ runtime adapters (Claude, Codex, OpenCode, Grok)
Skaphos is a project of Rillan AI LLC, a Missouri
limited liability company. © 2026 Rillan AI LLC. Released under
the MIT License.