repowise answers from its index, so the index should follow your code. Pick the method that fits your setup; they can be combined.
| Method | Best for | Requires server? |
|---|---|---|
| Post-commit hook | Solo developers, local repos | No |
| File watcher | Local development, uncommitted work | No |
| GitHub webhook | Teams, a shared repowise server | Yes |
| GitLab webhook | Teams, a shared repowise server | Yes |
| Gitea / Forgejo webhook | Self-hosted teams | Yes |
| Polling fallback | Safety net for missed webhooks | Yes |
Whichever you use, agents are told when the index is behind: the SessionStart
hook reports indexed commit vs HEAD (HOOKS.md), and MCP
responses carry a stale_warning when a file they served has changed.
Runs repowise update in the background after every local commit. Your
terminal is never blocked.
repowise init installs the hook by default (interactive runs ask first;
--no-hook skips it). To manage it yourself:
repowise hook install # current repo
repowise hook install --workspace # every workspace repo
repowise hook status # add --workspace to check every repo
repowise hook uninstall # add --workspace to remove from every repoThe hook is marker-delimited, so it coexists with other tools' hooks in the
same post-commit file.
After each commit, repowise update diffs the new commit against the last
synced one, refreshes the graph, git signals, dead code, health and decisions,
and regenerates the wiki pages the change affects (when the repo was indexed
with docs). See How updates work.
Watches the working directory and updates on save. Use it when you want the index current without committing.
repowise watch # current directory
repowise watch /path/to/repo
repowise watch --debounce 5000 # wait 5s after the last change (default: 2s)
repowise watch --index-only # no model calls per save
repowise watch --workspace # every workspace repoPress Ctrl+C to stop.
The watcher indexes the working tree, so staged, unstaged and untracked files
all reach the index without a commit. It ignores .repowise/, .git/,
node_modules/, build output and other blocklisted directories, lockfiles and
non-source files, and the files repowise manages itself (CLAUDE.md,
AGENTS.md, .mcp.json, .claude/), so an update never triggers itself.
On a repo indexed with docs, each trigger regenerates the changed files' pages
with a model. --index-only keeps the index current at no model cost and
leaves the prose for a later repowise update.
For a shared repowise server. GitHub sends a push event to the server, which runs an incremental update.
You need a server reachable from GitHub, and a webhook secret for any server exposed beyond localhost.
repowise serve # http://localhost:7337
repowise serve --host 0.0.0.0 --port 8080 # custom bind
export REPOWISE_GITHUB_WEBHOOK_SECRET="your-secret-here"In your GitHub repo, open Settings > Webhooks > Add webhook:
- Payload URL:
https://your-server.example.com/api/webhooks/github - Content type:
application/json - Secret: the value of
REPOWISE_GITHUB_WEBHOOK_SECRET - Events: Just the push event
Push a commit and check Recent Deliveries. A working setup returns 200
with {"event_id": "...", "status": "accepted"}.
With REPOWISE_GITHUB_WEBHOOK_SECRET set, every request must carry a valid
HMAC-SHA256 signature in X-Hub-Signature-256. A missing sha256= prefix or a
mismatched signature gets 401 Unauthorized.
Without the secret, the server accepts only local (loopback) callers and
rejects everything else with 403 Forbidden. That keeps repowise serve
usable for local testing, but it is not safe for a server reachable from the
internet. Set the secret before exposing the server.
Same idea as GitHub, with a different endpoint and token check.
export REPOWISE_GITLAB_WEBHOOK_TOKEN="your-token-here"In your GitLab project, open Settings > Webhooks:
- URL:
https://your-server.example.com/api/webhooks/gitlab - Secret token: the value of
REPOWISE_GITLAB_WEBHOOK_TOKEN - Trigger: Push events
With REPOWISE_GITLAB_WEBHOOK_TOKEN set, the server compares it with the
X-Gitlab-Token header in constant time and rejects a mismatch with
401 Unauthorized. Without the token, only local callers are accepted and every
other caller gets 403 Forbidden, as with GitHub.
One endpoint serves both: Forgejo is a Gitea fork and still sends the
X-Gitea-* headers alongside its own X-Forgejo-* ones.
export REPOWISE_GITEA_WEBHOOK_SECRET="your-secret-here"In your repository, open Settings > Webhooks > Add Webhook > Gitea (Forgejo on Forgejo):
- Target URL:
https://your-server.example.com/api/webhooks/gitea - HTTP Method:
POST - POST Content Type:
application/json - Secret: the value of
REPOWISE_GITEA_WEBHOOK_SECRET - Trigger On: Push Events
With REPOWISE_GITEA_WEBHOOK_SECRET set, every request must carry a valid
HMAC-SHA256 digest of the raw body in X-Gitea-Signature or
X-Forgejo-Signature. Both forges send it as bare lowercase hex with no
sha256= prefix, which is the one way their delivery differs from GitHub's;
a prefixed value is rejected with 401 Unauthorized, as is a mismatch.
Without the secret, only local callers are accepted and every other caller gets
403 Forbidden, as with GitHub and GitLab.
Gitea and Forgejo also send GitHub's X-Hub-Signature-256 for compatibility,
so an existing hook pointed at /api/webhooks/github keeps working — it is
just recorded as a GitHub event. Use this endpoint to have the delivery
attributed to the forge that sent it, and to give it its own secret.
All three endpoints match the payload's repository to a registered one by a canonical key, so clone, ssh and web URLs of the same repository match, case aside. Other hosts are listed in GIT_HOSTS.md.
While repowise serve runs, a background job polls every registered repository
every 15 minutes. New commits a webhook missed trigger an incremental update.
No configuration is needed.
- Diff the new
HEADagainst the last synced commit (or--since <ref>). - Re-parse and rebuild the dependency graph, re-index git metadata for the changed files, and refresh dead code, health and decision markers.
- Pick the affected wiki pages: the changed files' pages plus their direct
importers, within a budget scaled to the size of the change
(
--cascade-budget Nto override). - Regenerate those pages with the LLM, when the repo was indexed with docs.
Affected pages beyond the budget are marked stale, not regenerated;
repowise generate --stalerefreshes them when you choose to spend on it. - In a workspace, re-run the cross-repo analysis.
- Save the new
HEADas the last synced commit.
repowise update # diff since last sync
repowise update --since abc123 # diff from a specific commit
repowise update --dry-run # show affected pages, regenerate nothing
repowise update --cascade-budget 50 # allow more pages (default: auto)
repowise update --index-only # refresh the index, no model callsrepowise update --workspace updates every stale repo, then re-runs the
cross-repo analysis (co-changes, package dependencies, API contracts). Each repo
uses its own docs setting; --docs / --no-docs override it for the run.
--repo <alias> targets one repo. See WORKSPACES.md.
POST /api/repos/{id}/sync re-parses the repo, re-indexes git metadata, and
rescans dead code and decisions. It does not regenerate wiki pages, so it has no
LLM cost. POST /api/repos/{id}/full-resync also regenerates every page.
Rapid commits through the post-commit hook and a manual repowise update can
start updates at once. One update runs at a time; a second one that finds the lock held
records its commit and exits, and the running update rolls forward to it.
| File | Purpose |
|---|---|
.repowise/.update.lock |
Held by the running update. Records its PID, a process creation token and the target commit, so a lock left by a crashed update is cleared. A workspace update uses .repowise-workspace/.update.lock |
.repowise/.update.queued |
Written by the post-commit hook before it starts the background update, so agent hooks stop reporting a stale wiki right away |
.repowise/.update.pending |
Written by an update that found the lock held. Carries the newest HEAD, which the running update processes in an extra pass before it exits |
.repowise/.update.log |
Output of background updates. Capped at 256 KB, keeping the last 64 KB |
| Variable | Used by | Description |
|---|---|---|
REPOWISE_GITHUB_WEBHOOK_SECRET |
Server | HMAC secret for GitHub webhook verification |
REPOWISE_GITLAB_WEBHOOK_TOKEN |
Server | Token for GitLab webhook verification |
REPOWISE_GITEA_WEBHOOK_SECRET |
Server | HMAC secret for Gitea / Forgejo webhook verification |
Provider API keys, the database URL and the API token are listed in
CONFIG.md. Keys saved to
.repowise/.env during repowise init are loaded by update automatically.
"No previous sync found": run repowise init first.
"Already up to date": the index is already at the latest commit.
Hook doesn't fire: make sure the hook file is executable:
chmod +x .git/hooks/post-commit. repowise hook status shows whether it is
installed.
Webhook returns 401: a secret or token is set on the server but the
request's signature did not match. Check the value matches on both sides, and
the prefix: GitHub's X-Hub-Signature-256 needs the sha256= prefix, while
Gitea's and Forgejo's X-Gitea-Signature must not have one.
Webhook returns 403: no secret or token is set, and the request came from a
non-local caller. Set REPOWISE_GITHUB_WEBHOOK_SECRET,
REPOWISE_GITLAB_WEBHOOK_TOKEN or REPOWISE_GITEA_WEBHOOK_SECRET, restart the
server, and redeliver the payload.
Update is slow: the first update after many commits does more work.
repowise update --index-only skips page regeneration when you only need the
index current.