Create a Toggl Browser Integration (with Claude)
π€ This guide shows how to author a Toggl custom browser integration β the in-page Toggl "Track" timer button on a third-party site β using Claude instead of hand-writing the JSON. Worked example below: a GitLab issue board.
What you're building
A Toggl integration injects the Toggl button into a third-party website (GitHub, Jira, GitLab, Asanaβ¦) and teaches it how to read a task title + metadata from that page's DOM. An integration is a single JSON document. Claude drives the whole authoring loop β finding selectors, injecting the button on the live page, verifying placement β so you don't write selectors by hand.
Prerequisites
- Claude Code (any project folder works β the work happens in the browser, not the repo).
- The Toggl browser extension installed in Chrome and signed in, with a workspace/organization available.
- The Claude-in-Chrome extension connected (Claude drives the page through it).
- Recommended: for the session, enable only Claude-in-Chrome and the Toggl extension β disable others. Extensions like 1Password can silently intercept clicks/inputs Claude drives.
Step 0 β Install the skill (one time)
Install the Toggl skills globally so Claude knows the integration-authoring flow:
npx skills add toggl/skills
The CLI detects your installed agents and symlinks the skill into ~/.claude/skills/. This makes the toggl-browser-integrations skill available. Source: https://github.com/toggl/skills
Once installed, the skill triggers automatically when you ask Claude to add the Toggl button to a website.
Step 1 β Open the target page & ask Claude
- In Chrome, open the exact page/item you want the button on (e.g. a GitLab board, an issue detail, a list view) β already authenticated and in the right state.
- In Claude Code, ask it to build the integration, e.g.:
Create a Toggl browser integration on this page:
https://gitlab.com/<group>/<project>/-/boards - Claude selects your Chrome browser, navigates/confirms it's on the tab you see (it injects a banner + checks the tab is visible/focused), and inspects the page DOM β thinking about the page structure and probing elements for stable anchors and the text to track.
π‘ It works from any folder β you only need Claude Code running plus the skill and the browser. Nothing is committed to your repo.
Step 2 β Answer Claude's intent questions
Before writing JSON, Claude asks what it can't reliably infer:
- What to track as the task title? β e.g. issue number + title (
#43198 Auto DevOps callout bannerβ¦). - Any extra metadata? β project, labels, milestoneβ¦ or none (just the title).
- Single vs. one-per-card β Claude usually infers this from how many times the anchor matches (a board β one button per card; a detail view β one button). It states its inference; you only correct it if wrong.
In the worked example we chose #number + title and no metadata.
Step 3 β Open the Toggl side panel
Claude can't click browser chrome, so this part is you:
- Right-click the Toggl extension icon β "Open side panel" on the same tab Claude is driving.
- In the panel header ("Saving to"), pick an organization.
β οΈ The bridge is per-tab: the side panel must be open on the exact tab Claude is posting to, with an org selected β otherwise nothing injects. When it opens, the panel pulls your organization's integrations first. If the org already has one for this site, it loads that and a save updates it rather than creating a second one. If the site has none, the panel starts empty and Claude authors from scratch. If the panel says it couldn't check your organization's integrations, fix that before saving, or use Apply locally until it can.
Step 4 β Claude injects & verifies
Claude posts the definition to the page (via a shared-DOM bridge), which applies it locally and injects the button(s) immediately. Claude then verifies automatically (takes ~1β2 min):
- Counts the injected wrappers (e.g. 10/10 cards on the board).
- Confirms each button reads its own card's text (per-card scoping).
- Hit-tests that the button is actually clickable β boards often have a "click anywhere to open" overlay covering the card, and visible β clickable.
- Screenshots/zooms to check alignment and theming.
Step 5 β Tweak placement (iterate)
If the spot isn't right, just say so β e.g. "put it on the bottom-right of each card." Claude reworks the anchor and adds any CSS needed to beat the card's click-overlay (giving the button its own stacking context with isolation: isolate), reloads, re-posts, and re-verifies. Repeat until it's where you want it and reads correctly.
Step 6 β Test it
Open one of the items, click the injected Track button, confirm the tracked title is correct (e.g. #number + description), start tracking, then stop it. Confirm it behaves end-to-end.
Step 7 β Save (or keep local)
- Local draft: what Claude applied is a local draft β injected on this tab and it survives a page refresh, but isn't synced anywhere.
- Save to org: click Save & sync in the side panel to persist the integration to your organization (needs the manage-integrations permission). This enables it for everyone in the org. If the org already had an integration for this site, this replaces it under the same id.
π One integration can cover multiple pages. The board and the issue-detail view are different button placements in the same definition. You initially build for one surface; just continue the conversation ("also add it on the issue detail page") and Claude adds the extra button. Don't force one button to serve two surfaces.
Worked example recap β GitLab boards
| Aspect | Result |
|---|---|
| Site | gitlab.com issue board |
| Buttons | One per card (multiple: true) β 10/10 cards |
| Tracks | #<number> <title> (e.g. #43198 Auto DevOps callout bannerβ¦) |
| First placement | Inline after the title link |
| Final placement | Bottom-right of each card footer (.board-card-footer, append last) |
| Overlay handling | isolation: isolate β’ z-index so the button beats GitLab's card click-overlay |
| Theme | Follows page light/dark via themeDetector |
{
"name": "GitLab Boards",
"domain": "gitlab.com",
"selectors": { "card": "[data-testid=\"board-card\"]" },
"resolvers": {
"idText": { "closest": "@card", "query": "[data-testid=\"work-item-id-text\"]" },
"title": { "closest": "@card", "query": "[data-testid=\"board-card-title-link\"]" }
},
"buttons": [{
"name": "card-button",
"anchor": ".board-card-footer",
"append": "last",
"multiple": true,
"variant": "minimal",
"iconSize": 14,
"description": { "resolvers": ["idText", " ", "title"] }
}],
"cssContent": "[data-toggl-button-wrapper=\"card-button\"]{position:relative;isolation:isolate;z-index:1;margin-left:auto;align-self:center;}",
"themeDetector": { "colorElement": "body" }
}
Tips & gotchas
- Prefer stable selectors β
data-testid, roles, ARIA β over hashed CSS-module class names, which change between deploys. - List vs. detail are different buttons β separate entries with their own
anchor,multiple, and scoping. - Stretched click-overlays β cards that are "click anywhere to open" can cover your button even when it looks on top; the fix is a separate stacking context, not just a bigger
z-index. - If nothing injects β check the side panel is open on this tab, an org is selected, and only Claude + Toggl extensions are enabled.
- Per-tab bridge β keep the side panel and Claude on the same visible tab.