Is Teal - Job Search Companion safe?
Teal is medium risk. The extension fetches a config from S3 (teal.extension/config.v4.json) specifying which elements to read on 60+ job sites, including LinkedIn, Indeed, Glassdoor. It lives server-side, so scraped sites/fields can change without a new review.
Who publishes itTeal Labs, Inc - no other listings under this identity
Teal Labs, Inc - no other listings under this identity
What this publisher told the store about itself, and the other listings that told it the same thing.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Remote server controls what job-page data the extension scrapes
The extension fetches a config from S3 (teal.extension/config.v4.json) specifying which elements to read on 60+ job sites, including LinkedIn, Indeed, Glassdoor.
It lives server-side, so scraped sites/fields can change without a new review.
You open a job listing page on a site like LinkedIn, Indeed, or Glassdoor.
The extension's content script runs automatically on the page.
The extension fetches a remote configuration file that tells it, per site, which page elements to read for the job title, company, description, and compensation.
The rules that decide what gets read live on the vendor's server, not in the reviewed extension code.
content-scripts/content.js, fetching and caching the remote selector rules
const fetchRemoteConfig = async () =>
environment.value.config
? (await fetch(environment.value.config)).json()
: hardcodedFallbackConfig;
// webext-storage-cache wrapper: refetches config.v4.json at most every
// 3 hours, with a 1-hour stale-while-revalidate window.
const configCache = new StorageCache("config", {
maxAge: { hours: 3 },
staleWhileRevalidate: { hours: 1 },
updater: fetchRemoteConfig,
});
// Every scraping call site goes through this to get the current rules.
const getScraperConfig = () => configCache.get();// True if the current URL/DOM matches ANY server-supplied site rule.
const isSupportedPage = async () => {
const config = await getScraperConfig();
return matchesAnySiteRule(config, document.URL);
};
// True if the page matches the server's rule for page type "JOB" AND
// the rule's selectors actually find matching content in the DOM.
const isJobPage = async () => {
const config = await getScraperConfig();
const match = matchSiteConfig(config, PAGE_TYPE.JOB, document.URL);
return isTestOverrideActive()
? true
: !!(match?.siteMatch) && !!(match?.data);
};| Field | Value | Why it matters | |
|---|---|---|---|
Per-site selector rules | indeed.com/cmp/.+/(jobs|\?.*jk=) → job.role: "[data-testid='jobDetailTitle']" | CSS selectors marking which elements hold job title, company, description, location, and pay per site. | |
DOM change watchers | monster.com/jobs/, glassdoor.(com|sg|ca|co.uk)/Job/ | Tells the extension which page sections to keep watching for updates after the initial load, for sites that load job content dynamically. | |
Job-posting detector heuristics | knownUrls: "boards.greenhouse.io/.*/jobs/.*"; goodSubmitBtn matches "submit application" | URL patterns and page-text rules used to decide if the current page is worth scraping. | |
Covered domains | linkedin.com, indeed.com, glassdoor.com, google.com, dice.com, builtin.com, and 55 others | The list of job-search and career sites the extension is currently configured to read, including major boards and Google search results. |
- s3.amazonaws.com
Hosts Teal's config.v4.json file, an Amazon S3 object (not a Teal-branded domain) that lists the CSS-selector rules used to read job-listing pages across 61+ sites.
Fetches the extension's live remote configuration and lists every job-site domain it currently controls scraping behavior for, so you can check the current live scope without installing the extension.
#!/usr/bin/env python3
"""Fetch Teal's live remote scraper config and list every job-site domain
it currently controls DOM-extraction behavior for."""
import json
import urllib.request
CONFIG_URL = "https://s3.amazonaws.com/teal.extension/config.v4.json"
def main():
with urllib.request.urlopen(CONFIG_URL) as resp:
body = resp.read()
config = json.loads(body)
scraper = config.get("scraper", {})
domains = sorted({key.split("/")[0] for key in scraper})
print(f"Fetched {len(body)} bytes from {CONFIG_URL}")
print(f"{len(scraper)} site-path rules covering {len(domains)} unique domains:\n")
for domain in domains:
print(f" {domain}")
if __name__ == "__main__":
main()
- 1Run `python3 fetch_teal_config.py` (no install needed; publicly readable).
- 2Compare the domain list against what the CWS listing discloses.
What it can do
Permissions this extension asks for, as declared in version 4.0.8. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to.
Store data in your browser
storage
See the address and title of every tab you have open
tabs
Act on the current tab, but only after you click the extension
activeTab
Show a panel beside the page
sidePanel