No description
Find a file
Vincent Batts b87d6a49d6 Set each event's session to the profile's display name
Read the profile name from the browser's Local State and report it as
"<name> (<browser>)", falling back to the directory name. The name comes
first because gosh's timeline shows only the first 8 bytes of a session.
Refs keep using the directory name so renaming a profile does not
re-report its history. Only the name is read, not the account name or
email stored beside it.
2026-10-17 09:38:16 -04:00
.gitignore Add gosh-chrome plugin reporting Chromium-family browsing activity 2026-10-17 09:27:51 -04:00
chrome.go Set each event's session to the profile's display name 2026-10-17 09:38:16 -04:00
config.go Add gosh-chrome plugin reporting Chromium-family browsing activity 2026-10-17 09:27:51 -04:00
go.mod Add gosh-chrome plugin reporting Chromium-family browsing activity 2026-10-17 09:27:51 -04:00
go.sum Add gosh-chrome plugin reporting Chromium-family browsing activity 2026-10-17 09:27:51 -04:00
LICENSE Add gosh-chrome plugin reporting Chromium-family browsing activity 2026-10-17 09:27:51 -04:00
main.go Set each event's session to the profile's display name 2026-10-17 09:38:16 -04:00
main_test.go Set each event's session to the profile's display name 2026-10-17 09:38:16 -04:00
README.md Set each event's session to the profile's display name 2026-10-17 09:38:16 -04:00

gosh-chrome

A gosh plugin that reports Chrome browsing activity: when you were on which pages, and for how long. It reads every Chromium-family profile it finds (Chrome, Chromium, Brave, Edge, Vivaldi), since they share one history format. A sibling of gosh-firefox.

Each profile keeps history in a SQLite file called History. gosh-chrome collect turns it into gosh events:

kind one event per
visit page visit (visits)
view 5 minutes the page was in the foreground (context_annotations.total_foreground_duration, else visits.visit_duration)

A visit is a single moment, but gosh infers time from the spacing of events. The view events fill in the time Chrome measured on the page, so a 40 minute read counts as 40 minutes and not 5. Subframes and internal pages (chrome:, chrome-extension:) are skipped.

Install

go install git.batts.cloud/vbatts/gosh-chrome@latest
gosh plugin add chrome
gosh-chrome doctor        # are my profiles readable?
gosh collect

Profiles are found under ~/.config/{google-chrome,google-chrome-beta,google-chrome-unstable,chromium, BraveSoftware/Brave-Browser,microsoft-edge,vivaldi} (honouring XDG_CONFIG_HOME) and the snap and flatpak locations. --dir limits collect and doctor to one profile or one user data directory.

How time is decided

Chrome records two durations per visit, and neither means you were there:

  • total_foreground_duration is time the page was in the foreground. 0 means it never was (a tab opened in the background); -1 means it was never finalized.
  • visit_duration is time until the tab navigated or closed. For a background tab, that is how long the tab stayed open: in one real profile 883 visits claim over an hour, up to 96 hours.

So the plugin trusts the foreground duration when it is known (including 0: no time), and falls back to visit_duration only when it isn't. Then every visit is limited by a cap, 30 minutes by default: foreground time over the cap is clamped to it, but a fallback visit_duration over the cap is dropped, because it is almost always a tab left open, and clamping it would still invent attention. On one profile this is the difference between crediting 352 hours and 160. Set cap in the config to change it. doctor shows how much time was found and how many visits hit the cap.

The foreground duration is still the time a page was active in its window, not proof you were at the keyboard, but gosh only credits about 5 minutes per event and ends a session after a 30 minute gap.

Where activity is reported

gosh plugin protocol 2 lets an event's path be a URI, so each event's path is simply the page: https://git.batts.cloud/vbatts/gosh/issues. gosh drops the query, fragment and credentials, lowercases the host and groups everything in a tree by site and page:

gosh report -by path -only chrome -since 7d
gosh report -by path -only chrome -prefix https://github.com -depth 2

Pages opened from disk (file:///...) are reported as plain file paths, so they land in the git project that contains them. Files on another host are skipped.

Browsing has no git project, so gosh report -by project shows web pages under (no repo). To count browsing toward real projects, map sites to directories in ~/.config/gosh-chrome/config (or $XDG_CONFIG_HOME/gosh-chrome/config):

# host, host/path-prefix, or *.suffix, then the directory
git.batts.cloud/vbatts/gosh   ~/src/git.batts.cloud/vbatts/gosh
docs.example.com              ~/work/example
*.corp.example.com            ~/work/corp

ignore mail.example.com       # never reported
detail url                    # url (default), title, host or none
cap 30m                       # longest time credited to one visit

The most specific match wins (longest path prefix, then exact host over wildcard). A mapped page is reported at the directory, so it leaves the per-page tree; with detail url its URL is kept in the event's detail and shows in gosh timeline. Unmapped pages already carry the URL as their path, so they get no detail by default. gosh passes plugins no options, so this file is the only configuration.

Profiles

Every profile is read, and each event's session is <profile name> (<browser>), for example Aspen (google-chrome), using the name you gave the profile in the browser (from Local State; the directory name, like Profile 2, is the fallback). gosh timeline shows the first 8 bytes of the session, so you can tell work from personal browsing; the name comes first for that reason. Event refs use the directory name, which doesn't change when you rename a profile. Only the profile's name is read from Local State, not the account name or email stored next to it.

Privacy

The URL is the event's path and is shown in reports you may share. The plugin sends it without its query string, fragment or credentials, which is where tokens usually live, but paths can still hold ids (a document id, a mail message id). detail title adds page titles; ignore keeps a site out entirely. Profile names appear as the session, so name them with that in mind.

Synced history

Chrome Sync merges history from your other devices into visits. Unlike Firefox, Chrome marks these: visits.originator_cache_guid is set to the originating device and empty for visits made here. Synced visits are skipped, so visit events mean "browsed on this machine"; doctor shows how many there are. The column is missing from older Chrome, where synced visits are reported.

When Chrome changes the format

The History schema is internal to Chrome and not a stable API (meta.version; this was tested with 70, Chrome 155). So the plugin does not trust the version number:

  • Every run checks that the tables and columns it queries exist. If they don't, that profile is skipped with the missing names on stderr, and if no profile could be read, collect exits non-zero so gosh stores nothing rather than leaving a quiet gap.
  • A newer version than tested is fine while the columns check out; doctor mentions it.
  • Without originator_cache_guid or context_annotations, you still get visits, with the fallbacks above.
  • Timestamps are range-checked (2000 to tomorrow). If the epoch or unit changes, the rows are dropped with a warning instead of being reported at the wrong time. A full run (--since 0) applies no lower bound in SQL precisely so such rows are seen and flagged, not silently filtered out.

gosh-chrome doctor shows each profile's path, browser version, schema version, what was found and status: ok or what is wrong; it exits non-zero if any profile is unusable.

How it reads a live database

Chrome keeps History open and locked while it runs, with recent changes possibly in History-journal. Each run copies the database and its journal to a temporary directory, reads the copy, and deletes it. Profiles not written since --since are skipped, so frequent runs are cheap. A copy taken in the middle of a write can be inconsistent; if so that profile fails with a warning and the next run reads it normally.

Limitations

  • view events are spread from the visit's start onward, but Chrome only records a total, so a page you left and returned to hours later is credited as if you stayed.
  • Incognito and guest browsing is not recorded in History.
  • Only Linux locations are known.

Protocol

gosh-chrome describe
gosh-chrome collect --since <unix seconds> [--dir <profile or user data dir>] [--config <file>]
gosh-chrome doctor [--dir <profile or user data dir>] [--config <file>]

The plugin declares protocol 2 (paths may be URIs), so it needs a gosh that supports it. Output is JSON lines; see gosh's docs/PLUGINS.md. Events are keyed by browser, profile and visit, so collecting twice is harmless. The only dependency is the pure-Go SQLite driver modernc.org/sqlite (no cgo).

License

MIT, see LICENSE.