GitPulse is a local-first, zero-telemetry developer desktop application. Repository and task state stay in local stores. Git remotes, GitHub operations, optional tool installation and configured agent providers can use the network.
Product stack. DevCouncil is components and modules. Manvi wraps them. GitPulse uses Manvi for policy, workbench, and agent hosting, and selected DevCouncil modules for code intelligence. Take or update only the modules this app needs.
flowchart TD
subgraph Boundary["Security & Isolation Boundary"]
direction TB
Webview["Tauri Webview<br/>(Strict CSP: default-src 'self')"]
IPCBoundary["Tauri IPC Seam<br/>(241 Policy-Checked Custom cmd_* Handlers)"]
LocalEngine["Rust Core Engine<br/>(Validated paths, git sandbox & policy gates)"]
Webview -->|Typed IPC Only| IPCBoundary
IPCBoundary --> LocalEngine
end
subgraph SidecarIsolation["Local Sidecar & Module Boundaries"]
direction TB
ManviSidecar["Manvi Sidecar (manvi serve)<br/>(Stdio NDJSON, 5-verdict safety ladder)"]
DevMapGate["DevMap Engine<br/>(Local .devmap/ in $GIT_COMMON_DIR/info/exclude)"]
end
subgraph ExternalSurfaces["External Surface Isolation"]
direction TB
LocalGH["Local gh CLI<br/>(Uses existing OS keychain)"]
LocalFB["Local firebase CLI<br/>(Uses existing local login)"]
LocalAI["Local LLM Server<br/>(Loopback 127.0.0.1 / localhost Only)"]
PTY["User Shell / Explicit Agent PTYs<br/>(Separate OS process trees)"]
end
LocalEngine <-->|Stdio Only| ManviSidecar
LocalEngine <-->|Local Store & Socket| DevMapGate
LocalEngine --> LocalGH
LocalEngine --> LocalFB
LocalEngine --> LocalAI
LocalEngine --> PTY
- GitPulse has no centralized backend or analytics tracking.
- Network-capable features run through user actions or explicitly enabled settings,
including scheduled release checks, automatic task suggestions, and the
default-on GitHub alert scan (Dependabot and code scanning via the local
ghCLI when a repository opens). A failed GitHub check does not toast. - Firebase App Hosting listings are user-action only, never automatic, because
they can enable an API on your Google Cloud project. See Local
firebaseCredential Safety below. - The webview does not load external CDN scripts, styles, or telemetry trackers.
- Opening a checkout first requires an explicit Trust and Open decision. Path inspection does not run Git; canceled or failed approval does not start watchers, status hydration, dependency scans, or code indexing. A tab whose open was refused for trust is marked as needing trust and is left out of live indexing, DevCouncil initialization, documentation refresh, metrics and status polling, so a refusal stays one state instead of a stream of failing calls. A workspace registry sync still writes the registry and reports such tabs as skipped for being untrusted.
- Trust permits the repository's Git hooks, helpers, and project tools to run with the user's account permissions, including code in submodules. It is an execution decision, not an OS sandbox or a claim that the project is safe.
- GitPulse stores remembered grants in its application configuration directory, outside repository policy and task files. A grant binds the repository — the common Git directory, by canonical path and filesystem identity — together with the checkout that was approved. Replacing either, or redirecting a gitfile at a different repository, requires fresh approval.
- Identity is compared in a way that survives a reboot. On Unix it is the canonical path, the inode and the birth time; on Windows, the file identity. The device number is deliberately not compared, because macOS reassigns it at mount and a reboot renumbers it, which used to revoke every approval. The birth time still tells a replacement from the original when an inode is recycled. The trade-off is stated plainly: a same-user attacker who mounts a crafted volume over the exact approved path could match an approval, but that user can already edit the owner-only trust store, and nothing widens which repositories are trusted.
- Grant records are named by an FNV-1a hash of the path, which a Rust release
cannot change (the earlier
DefaultHashernames could). Lookup still reads the old names, and a bounded startup pass (at most 4,096 directory entries) renames what it finds, skipping symlinks, fifos, corrupt and oversized files. A damaged record under the stable name fails closed rather than falling back to an older one. - One approval covers the repository, so a linked worktree is not a second decision. Every working tree shares one configuration, one hook directory and one object database, and that shared surface is the whole of what the decision is about; approving per checkout would ask the same question again for no additional authority. The approval dialog names that scope before it is granted.
- A checkout is covered only when the approved Git directory itself vouches for
it: the repository is held inside it as a real
.gitdirectory, or its private Git directory is a registered entry under that common directory'sworktrees/whosegitdirnames this checkout back. A directory that merely points a gitfile or a symlink at a trusted common directory is making a claim, not presenting evidence, and is refused — forging what is read instead requires write access to the approved repository's own Git directory, where acore.fsmonitorwould already run. - Shared Git-directory discovery stays filesystem-only, and removing a worktree still admits the target in its own right because Git scans it internally. A target in the approved repository is answered by that same grant; a target in a repository nobody approved is refused before Git starts.
- Revocation reaches the whole repository, including approvals recorded separately for its other working trees, so it is never partial. If the checkout has already been deleted it can no longer name its repository, so revocation falls back to the checkout's own path, which is all that remains to identify.
- Approvals stored before the repository became the unit are still honoured and are not widened by the change: each authorizes only the checkout it named, read from its own filename namespace so it cannot be replayed as a repository-wide one. Widening a decision taken under narrower terms is a decision to re-take, not to reinterpret, so the first approval granted after upgrading is the one that covers the family.
- That re-taking is offered rather than waited for. Trust is reported as a scope — none, this checkout, or the repository — not as a yes/no, because a single bit cannot distinguish a complete approval from a partial one, and a caller reading one reports "already trusted" for a repository whose every worktree is refused. Where a pre-repository approval leaves siblings unreadable, the worktrees panel says so and offers to extend it, and the refusal a linked worktree returns names the older scheme instead of telling someone to grant an approval they already granted. Extending is still the human's decision and still goes through the same dialog; nothing is widened by reading it.
- Native Git, background analysis, DevMap, PTY startup, and MCP reads enforce trust. MCP cannot grant it. A refusal is an explicit error or unavailable result, never a successful empty scan.
- Global tool probes run from a neutral filesystem root. Local source-tool installation requires approval for the checkout used to build the tool.
- Approval binds a directory, not a path. Because the operating system resolves a working-directory path again when a child starts, GitPulse pins the approved directory and starts the child on that pinned directory, so renaming a path component and leaving a link in its place cannot divert a Git or tool process into a checkout that was never approved. Where the whole path is already beyond other users' reach — every component owned by this user or root, and not writable by group or other unless it is sticky — the pin is redundant and is skipped, which keeps the common case on the fast process-launch path.
- On that pinned path the repository a child belongs to is read back through the pinned directory itself rather than by walking the path a second time, and the approval must name that same repository. A rename that briefly substitutes a directory and puts it back can otherwise leave the two descriptions disagreeing, with the child anchored to one checkout while the approval was granted for another.
- A terminal is the one child that cannot be re-anchored after it starts, so it is refused outright when a directory holding some component of the checkout's path is writable by other users. That refusal names the directory to change and is deliberately distinct from a trust refusal: approving the repository again cannot resolve it.
- Use the repository tab's Revoke repository trust action to close the tab and block subsequent operations for that repository and every worktree of it. Already-started terminals and agent tasks retain the authority they were given; stop them separately when needed.
- Filesystems that cannot supply a stable creation identity are refused with an explanation. Trust decisions are local to this installation and are not portable project metadata.
- Ordinary editor and documentation saves replace file entries atomically under pinned, symlink-free parent directories. Saving one hard link does not truncate the inode shared by its other names.
- Saves enforce the existing 8 MiB file budget. Nested paths, empty files, internal symlinks, and executable permissions remain supported; external symlinks and special files are refused.
- macOS metadata copying, Linux ownership and extended-attribute copying, and Windows replacement preserve supported file metadata. If preservation fails, the save fails rather than silently dropping that metadata. Linux saves clear set-id and file-capability privileges as ordinary content writes do; metadata work is bounded to 512 attributes and 1 MiB of values.
- Concurrent changes can make a save fail. If a change races publication, the error identifies retained recovery content for review. This is not an OS sandbox against another process already running with the user's rights.
- GitPulse never requests, reads, stores, or transmits your GitHub personal access tokens or passwords.
- All GitHub operations (PR inspection, workflow dispatch, Dependabot and code scanning queries) delegate exclusively to your locally installed and authenticated
ghCLI.
- The same rule holds for Firebase: GitPulse never requests, reads, stores or
transmits your Google credentials. App Hosting reads delegate to your locally
installed and authenticated
firebaseCLI, which owns the login. - Detecting that a repository uses Firebase is a local file read (
.firebaserc,firebase.json) and reaches no network. Those paths are resolved through the repository sandbox, so a symlinked.firebaserccannot be used to read a file outside the checkout — including the CLI's own credential store. - Listing backends or rollouts can enable an API on your Google Cloud
project. The Firebase CLI runs
ensureApiEnabledbefore those listings and turns the App Hosting API on when it is off. App Hosting publishes no read-only OAuth scope, so this is not avoidable by asking differently. GitPulse therefore treats those listings as mutations: they never run on their own, only from an explicit action, the panel states the side effect before the button, and the exact command line is judged by the policy gate before any process starts. Every invocation passes--json, which the CLI treats as implying non-interactive, so a prompt fails loudly instead of enabling something quietly. - A listing that could not run is reported as a reason, never as a backend with no deployments.
- What the CLI can do is asked, not assumed.
apphosting:rollouts:listis registered upstream only behind theinternaltestingexperiment, which is off by default, and an unregistered subcommand exits non-zero having written nothing to either stream. GitPulse therefore probes the installed CLI — by listing a command group, which needs no login, no project and no network — and offers the rollout action only when the subcommand is really there. A capability that could not be probed is reported as unknown rather than as absent: the two have different remedies. - Only three
firebasesubcommands can ever be spawned, each built by a single argv builder that includes the program name, so the line the policy gate judges is the line that runs. A contract test reads the module and fails on any subcommand literal outside that allow-list, on any flag the subcommand does not declare, and on--token, which would place a credential on a command line that other local processes can read.
apphosting:rollouts:createis the only command in this module that changes what the user's production traffic serves. It is not reversible from GitPulse — App Hosting publishes no rollback verb, so undoing a rollout means creating another against an earlier commit — and it is not idempotent: upstream allocates a new rollout id per call, so running it twice deploys twice.- It is therefore behind two steps, not one. The commit must be a full 40-character SHA (an abbreviation is an ambiguous target for something that reaches production, and is refused with the reason), and the panel spells out the exact project, backend and commit — plus the fact that GitPulse cannot undo it — before the deploying button exists at all. Changing any part of that target disarms the confirmation, so the values sent are always the values reviewed.
- The gate runs before the process. A refused verdict returns an error and nothing is spawned; a contract test asserts that ordering directly, because this command's argv is built in the Firebase module rather than spelled out in the handler and is therefore exempt from the literal-argv comparison.
--forceis never passed. Upstream prompts only when neither a branch nor a commit was named, and GitPulse always names a commit, so nothing here suppresses a confirmation the user would otherwise have seen.- A rollout that started is never reported as failed. If the CLI exits zero without confirming the result, the panel says the rollout started but could not be confirmed, and warns against a blind retry — because a false failure is what turns one deployment into two.
- Built-in local AI completions (commit messages, explanations and branch names)
and local model probes restrict their transport to loopback addresses
(
127.0.0.1,localhost,[::1]); remote base URLs are rejected on that path. - Commit messages are classified in the app from the staged patch. When no loopback model is selected, a supported Mac may ask Apple Intelligence only for the subject, and only from that classified brief — the patch itself is not sent. If that model is unavailable or its subject fails those checks, the message is the one the app already wrote. Explanations, branch names, and remediation plans still require a loopback model.
- Task enhancement and agent execution use the separately configured profile Manvi provider or agent CLI. Their network and filesystem access follow that provider and the selected run settings. The local-completion transport restriction does not establish containment for an explicitly launched agent.
- The embedded interactive terminal (
src-tauri/src/terminal/) runs user shell processes directly viaportable-pty. - Local AI suggestions and the policy sidecar do not read/write ordinary shell sessions. Explicit Claude, Manvi, Codex and task launches create dedicated PTY sessions; the launched process receives that session's input.
- Terminal handoffs remain user-controlled. Managed runs (Codex and Claude Code) use the separate Manvi configuration/decision protocol. A worktree is not an OS sandbox, and native launch validation alone does not prove the provider's effective policy. See Tasks and workspaces for the verification limits.
- Model-assisted remediation actions (
cmd_manvi_run_action) are restricted to a strict command allowlist and require explicit user confirmation.
- Opening a repository fetches Dependabot and code scanning alerts through the
locally authenticated
ghCLI. This is on by default under Settings → Analysis. GitPulse still does not read or store GitHub tokens. - Only critical and high findings raise a warning. A check that could not run
(missing
gh, not a GitHub remote, API error) is listed in Health and recorded in diagnostics, never presented as an all-clear. - Scan local on the Health panel does not call GitHub. The Check GitHub alerts button remains a manual refresh.
- Health's local dependency scan runs whichever audit CLIs are already on
PATH, and each is invoked so that it reads dependency metadata instead of
running the checkout: npm with
ignore-scripts, Composer with--no-plugins --no-scriptsso the project's ownvendor/plugins are not activated, pip-audit with--no-deps --disable-pip, cargo-audit through its subcommand binary so an[alias]in the checkout's Cargo configuration cannot shadow it, and govulncheck withGOTOOLCHAIN=localso a module's owntoolchainline cannot choose which Go toolchain is downloaded and run. Supply-chain health parsers (cargo denySARIF andcargo crevJSONL) similarly parse static artifact outputs without executing unverified code. - A scan that cannot complete is reported as a failed check, never as a clean result. These are bounded read-only invocations, not a sandbox: each scanner still parses repository-supplied files with the user's account permissions.
- Insights → Secrets integrates the Kingfisher scanner to discover exposed API keys, access tokens, and credentials in repository files.
- Kingfisher runs with
--redactand--no-validate, a scrubbed environment (PATH,HOME,NO_COLORonly), and a fixed argv that never loads a repository config. Findings cross IPC as rule ID and name, repo-relative path, line, confidence, Git location, and a per-scan ordinal grouping rows that hold the same value. Secret values, matched strings, context lines, Kingfisher fingerprints and redaction hashes are dropped in the backend and never cached or persisted. - Scanner standard output is unlogged in diagnostics to eliminate accidental secret disclosure in application logs or bug reports.
- Fail-closed reporting: scans that time out, error, abort, or truncate are reported as unverified / not clean, never as an all-clear. Kingfisher exits 0 when it cannot read a file, so completeness is taken from its audit record; a scan it marks partial, or one that carries no audit, is never shown as clean.
- Dead-branch cleanup identifies merged, stale, or squashed branches using
git merge-treeto verify merge status safely. - Before executing any deletion, GitPulse generates explicit git commands
(
git branch -d/-D,git push --delete) and requires user authorization. A refusal halts the entire batch before any branch is removed. - Durable tip backups: prior to any branch deletion, a restorable tip backup is written to disk and recorded in the event ledger, allowing recovery via the Restore action.
.gitpulse/hooks.tomlsupportspost_create,pre_merge, andpost_mergelifecycle commands.- Lifecycle commands run with the user's permissions and are strictly gated on explicit repository trust. Untrusted checkouts unconditionally refuse to execute lifecycle hooks.
- Hooks are bounded: each command has a 15-minute wall-clock deadline, keeps at most 1 MiB of its output, and has its whole process tree killed at the deadline, so a hung hook cannot hang worktree creation. The deadline limits duration, not authority: a trusted hook still runs with the user's rights.
- A hook that fails, times out or is refused is reported, never discarded: the
Worktrees panel says the worktree exists but was not set up, a teardown says
its
post_mergecleanup did not run, and an agent's worktree whosepost_createfailed is removed and the agent is not started. - Merge & teardown asks the policy gate about every git command it runs, as it runs it: the merge of the worktree's own branch, the squash commit, the forced worktree removal and the branch deletion. It refuses when the main checkout is not on the target branch, and a squash whose commit fails stops before the worktree or branch is touched.
- Automatic application release checks are off by default; GitPulse does not automatically install application updates.
- When explicitly enabled under Settings → Updates, GitPulse compares public release tags once a day via
git ls-remoteagainst the upstream repository. - No user tokens, repository paths, or hardware telemetry are ever sent.
- Release checks only notify with a link to GitHub releases. Optional-tool setup is a separate, explicitly requested install/update workflow.
The webview operates under a strict CSP configured in src-tauri/tauri.conf.json:
default-src 'self'connect-src 'self' ipc: http://ipc.localhostimg-src 'self' data: blob: https:— pictures, and only pictures, may load from any https origin, so a README's badges show. Fetching one reveals the viewer's IP address and the time to that host, the same as opening the README on a forge; rendered<img>elements carryreferrerpolicy="no-referrer", and plain-http pictures are not fetched. No directive other thanimg-srcadmits anhttps:source or a wildcard (scripts/dev-port.test.tsholds the CSP to exactly this).script-src 'self'— remote scripts, inline scripts andevalare disallowed.style-src 'self' 'unsafe-inline'— stylesheets load only from the app itself; inline styles are allowed. This is the only directive that permits inline content, and it still admits no remote stylesheet.font-src 'self' data:,base-uri 'none'andobject-src 'none'— no remote fonts, no<base>rewriting of relative URLs, and no plugin embeds.- The development policy (
devCsp) adds only the local Vite server'shttpandwsorigins on loopback; release builds never carry them.
Rendered repository Markdown is untrusted content in the app's own document.
MarkDev's renderer escapes raw HTML and refuses active URL schemes; GitPulse
then namespaces every rendered id, replaces any picture that would resolve
against the app's own origin, and handles every link itself — the webview
never follows one (src/lib/files/markdownLinks.ts, guarded by
harness/markdown.html). Pictures beside a note are read only from inside its
repository, through the same containment check as cmd_get_file_blob.
If you discover a security vulnerability in GitPulse, please report it via GitHub Security Advisories rather than filing a public issue:
We take security issues seriously and will respond promptly to investigate and patch confirmed vulnerabilities.
The September 2026 GTK migration replaced the old glib 0.18.5 dependency
with glib 0.22.9; the previous unresolved RUSTSEC-2024-0429 note no longer
matches the lockfile. The maintained GTK consumer patches and their validation
are documented in Dependency Health.
On 2026-09-12, the pinned revision ac84b24 passed cargo audit --deny warnings
for 550 Rust dependencies and npm audit for 175 npm dependencies, with no
reported advisories. These are dated advisory-database checks, not exhaustive
source or runtime security verification. Re-run both checks for a release.