Please report a vulnerability privately, through GitHub's Security → Report a vulnerability on this repository, not in a public issue.
Every push and pull request runs the checks below
(.github/workflows/docker.yml). A release is a version tag such as
v0.14.0: only a tag publishes an image, only when every check passes, and
only after the owner approves it in the release environment. A push to
main is built and checked, but not published.
| Check | What it catches |
|---|---|
| Tests and lint, Python 3.10 and 3.14 | Broken behaviour, including the dashboard's Host, token and CSRF guards |
Every CI install from requirements-ci.lock, with --require-hashes |
A package changed on PyPI, or one not listed, entering a test, a scan or the guide build: the run stops. The lock is audited with pip-audit too |
pip-audit on requirements.lock |
A dependency with a published vulnerability |
pip-audit on requirements-desktop.lock, every platform's packages |
The same, for what the desktop installers are built from (pyobjc on macOS, pythonnet on Windows included) |
bandit over the code |
Injection, unsafe calls and similar in jobdork's own code. A finding judged safe is marked in the code with its reason |
gitleaks over the whole git history |
A key, token or password ever committed, even if deleted since |
zizmor over the workflows |
Template injection, credentials left in a checkout, over-broad token permissions and risky triggers in .github/workflows; and, online, that every pinned commit is really its action's and not a known-vulnerable version. Medium and above fail |
hadolint |
Dockerfile mistakes |
| Trivy on the built image | High and critical vulnerabilities in every package in the image, fixed or not, plus secrets and misconfiguration |
scripts/check_image.sh |
That the image runs as user 1000, has no pip, contains no .env, config or database, cannot rewrite its own code, and that the dashboard starts and refuses a missing token and an address not named |
Dependency Review, on every pull request (dependency-review.yml) |
A dependency the pull request adds or upgrades with a known vulnerability of moderate severity or above; it fails, and comments which. It reads pyproject.toml; the locks are pip-audit's |
OpenSSF Scorecard, weekly and on every push to main (scorecard.yml) |
How the supply chain is guarded: pinned actions, branch protection, signed releases, reviews, token permissions. Findings go to Security, Code scanning; the score is published to scorecard.dev |
Desktop app build (desktop.yml), macOS (Apple Silicon and Intel), Windows and Linux (AppImage and .deb), after the tests and scans pass: on every release tag, on a change to what the installers are built from (packaging/, requirements-desktop.lock, pyproject.toml, the workflows, the app-only code), and by hand (Run workflow) |
That every installer still build from the hash-pinned requirements-desktop.lock, and that the built app carries every module and data file it needs and can verify a build attestation (its --self-test) |
How the image is built to be hard to tamper with:
- Every dependency is pinned with its checksum (
requirements.lock) and installed with--require-hashes: a tampered release on PyPI does not match and is refused. - The base image is pinned by digest, which names exact contents; a tag can be moved to point at something else.
- Dependabot waits a week before proposing a new version of an action,
the base image or a Python package (
.github/dependabot.yml). A malicious release is usually found and pulled within days, and the checksums above only stop a tampered copy of a version already pinned. Fixes for a published advisory are not held back. - Every GitHub Action is pinned to a commit, not a tag. The scanners run from their official images, also pinned by digest.
- The published image is signed with Sigstore keyless signing: the signature ties it to this repository's workflow and commit, is checked against GitHub's identity, and is recorded in a public log. There is no signing key that could be stolen.
- Each image carries a provenance record (which source, commit and workflow built it) and a software bill of materials (every package in it), attached in the registry.
- The workflow's token can only read the repository; publishing, signing and the Docker Hub token are confined to the one approved job.
- The weekly boards re-check (
.github/workflows/boards.yml) is the one workflow that writes: it pushes a branch and opens a pull request, withBOARDS_PR_TOKEN, a fine-grained token for this repository only. Its pull request goes through the same required checks and the owner's review as any other; nothing it does reachesmainor an image by itself.
- Set the new version with
python scripts/set_version.py <new version>, which writes it intopyproject.tomland into every page that names the release to copy (the image tag, the installers' names, the tag command below), and merge it intomainthrough a pull request; the checks must pass, and they fail if any page still names the old release. The tag in step 2 must match it: a tag that does not is stopped before the approval step. - After the merge, bring
mainup to date, then tag it and push the tag:git checkout main && git pull, then check thatmainnow says the new version,grep -m1 '^version' pyproject.toml, and only thengit tag v0.16.3 && git push origin v0.16.3. Only an admin can create av*tag, and once pushed it can never be moved or deleted, so a tag made before the pull lands on the old commit for good (asv0.14.0,v0.14.1andv0.14.2did). Pushing a branch or opening a pull request does not put anything onmain; merging it does.python scripts/tag_release.pydoes this step and checks it first: you say major, minor or patch, it counts the version on from the last release tag, and it tags only when that is the versionmainsays, the last commit is the merge ofrelease/<version>and every check on it passed. It then opens the run for step 3. - The workflow runs every check again on the tag, then stops at Review deployments: open the run under Actions and approve it. Nothing is published, and the Docker Hub token is not handed out, until you do.
- It publishes the image for Intel and ARM, signs it, verifies the
signature, and creates the GitHub Release page for the tag: the exact
image digest, the command to verify it, the desktop installers (the
macOS
.dmg, the Windows setup.exe, the Linux.AppImageand.deb, withSHA256SUMS.txtand how to get past the unsigned warnings), and what changed: thedocs/CHANGELOG.mdentries written since the last release, in full (scripts/release_notes.py), not a list of pull request titles. The installers are not signed, but each carries a build provenance attestation:gh attestation verify <file> --repo jessn-dev/jobDorkshows this repository's workflow built it from the tagged commit. From 0.15.4 the attestation's signed Sigstore bundle is on the release page beside each installer, as<file>.sigstore.json, so--bundle <file>.sigstore.jsonchecks it without asking GitHub.
Settings → Updates → Update in the desktop app downloads the installer
for this computer and puts it in place: on a Mac the new jobdork.app is
copied out of the disk image beside the running one and swapped in once
jobdork has quit, then opened; on Windows the installer runs on its own and
opens the new version; on Linux a new AppImage is swapped in the same way,
and a .deb is installed by pkexec apt-get install once jobdork has quit
(the desktop asks for the person's password; apt installs only that file).
It puts nothing in place, and opens nothing, unless
all of these hold (jobdork/core/updates.py):
- The file was uploaded by the release workflow. Release files uploaded
by anyone else (
uploaderother thangithub-actions[bot]) are not offered at all. A stolen login or token can add a file to a release, but it shows as uploaded by that person. - It matches
SHA256SUMS.txtfrom the same release. This only proves the download arrived whole. - Its build attestation verifies. GitHub's attestation for the file's
SHA-256 is checked with Sigstore: the signing certificate must be issued
by GitHub Actions to this repository's
desktop.yml, run for exactly that version's tag (refs/tags/v<version>), with a valid transparency log entry, and the signed statement must name this file and digest. A build from another workflow, branch, fork or tag fails, and so does any file whose bytes differ.
On a Mac the copied app must also say it is the version the release names
(its Info.plist), or it is deleted and nothing is swapped. The swap is a
rename in the app's own folder, which the person can write: nothing asks
for a password or runs as anyone else, and the old app is kept until the new
one is in its place. A folder the person cannot write, or an app run from
the disk image or from Downloads, gets the disk image opened instead.
A file that fails is deleted. GitHub unreachable while checking is a failure too, never a pass. Sigstore's trust root is refreshed online, or taken from the copy inside the app; the app's self-test verifies an earlier release's real attestation offline, so a build that cannot check is caught before release. Releases are immutable (see below), so a published file cannot be swapped later.
Check that it was built by this repository's workflow and not altered since:
cosign verify jessengolab/jobdork:0.16.3 \
--certificate-identity-regexp '^https://github.com/jessn-dev/jobDork/\.github/workflows/docker\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com(Install cosign.)
Then run it by digest, the sha256:… the verification printed, rather
than by tag:
docker pull jessengolab/jobdork@sha256:<digest>A tag can be re-pointed later; a digest always means the exact image you verified.
These cannot be set from the repository's files. Without them, someone who got into the GitHub or Docker Hub account could get around everything above.
Set on 2026-09-29:
- Environment
release: the owner is a required reviewer, and it can only be deployed fromv*tags. - Ruleset for
main: no deletion or force push; changes by pull request; the checkstest (3.10),test (3.14),securityandimagemust pass. - Ruleset for
v*tags: never updated or deleted, by anyone. - Ruleset for
v*tags: only a repository admin may create one. - Workflow permissions read-only; Actions cannot approve pull requests.
- Secret scanning and push protection (a key is stopped before it is pushed).
- Dependabot alerts and security updates.
- Private vulnerability reporting.
Set on 2026-10-23:
- Code scanning: CodeQL default setup (Python, JavaScript and the
workflows), on every pull request and push to
main, and weekly. - Passkeys on the owner's GitHub and Docker Hub accounts.
Set, confirmed 2026-10-02:
- A Docker Hub personal access token for this workflow only, with the
Read & Write scope (not Delete), saved as the secret
DOCKERHUB_TOKENin thereleaseenvironment, not in the repository. Rotate it yearly, and revoke it at once if it may have leaked. - The variable
DOCKERHUB_USERNAME, the Docker Hub account, in thereleaseenvironment (not at repository level).
Set on 2026-10-25:
- Ruleset for
main: CodeQL results required; a pull request that adds a high or critical security alert, or an error, cannot be merged. - Immutable releases: a published release's files can never be changed or replaced.
- Actions: only GitHub's own and the six named in the workflows
(
docker/*five,sigstore/cosign-installer), each pinned to a full commit SHA, which is now required. - Workflows from every outside contributor wait for approval.
- Wiki and Projects off; merged branches deleted.
-
BOARDS_PR_TOKEN, for the weekly boards re-check: a fine-grained personal access token for this repository only, with Contents and Pull requests set to Read and write and nothing else. Rotate it yearly.
To set (2026-10-28), by the owner:
- Actions, allowed actions: add
ossf/scorecard-action@*, so the Scorecard workflow may run (every other action it uses is GitHub's). - Code scanning, CodeQL: query suite Extended, for more security queries on the scan that already runs.
Not available on this account: secret scanning's generic patterns and validity checks need GitHub's paid Secret Protection; provider patterns and push protection are on.
Every job post is written by someone unknown: its title, its text and its links. The app treats them as hostile.
- In the dashboard every piece of it is escaped before it is shown, and
a link opens only if it is a web address (
http:orhttps:), so ajavascript:link does nothing. The page loads nothing from anywhere else (its Content-Security-Policy).tests/test_page_safety.pyfails on the two patterns that once let a link run script. - In the AI model's prompt it sits between markers it cannot close, labelled as data, and without the characters a reader cannot see (zero-width spaces, direction overrides, the tag characters that can spell out a hidden instruction), so the model reads only what a person reading the post could. The model's answers are text shown to you; nothing it says is run.
- For a draft by Claude Code (
claude -p,jobdork/writing/generate.py) the agent works in that one job's folder with file tools only: no shell, no web, none of your MCP servers, whatever your own Claude Code settings allow; and it reads none of those settings (--setting-sources project), so your own permission rules, hooks and plugins do not apply to it. A posting that persuades it can spoil that one draft, not reach the rest of your machine. - For a coding agent (Claude Code, Codex, Cursor and others, through
jobdork's MCP server or
jobdork agent,jobdork/web/agent.py) it is data, never instructions: the characters a reader cannot see are removed from every title, company, place and advert the agent is given, each answer carrying them says they are from strangers, and the agent is told never to follow them, run a command or open a link because of them. What the agent writes goes through every check a model's does, and a second model checks its claims, never the agent itself. It cannot add to your record without your OK on the Resume page (so a posting that persuades it cannot plant a skill it then cites), it has no tool that sends anything, and its writes are limited per hour, capped in size and logged on the Dashboard as its own. - As an address to fetch (an apply link, a careers page) it reaches
public addresses only. One that names this machine or its network
(
localhost,192.168.x.x,169.254.169.254), or redirects there, is refused (jobdork/fetch/http.py), so a posting cannot use the app to reach your router, printer or NAS. The connection goes to the very address that was checked, not to a second lookup of the name, so a site that answers the check with a public address and the connection with a private one (DNS rebinding) reaches nothing either. - In the desktop app the page can ask the app for three things only:
open a web address in your browser, save a file where you choose, and
show a PDF. A file is shown only if it really is a PDF, and always as
.pdf, so nothing handed to the app can run (jobdork/web/app.py). The page's policy still refuseseval, in the app as in a browser.
The desktop app's Ask page (jobdork/ai/ask.py) sends a question you
type to Claude Code on your computer. It is held to your job search, and
to jobdork, whatever the question says:
- It is checked before it is sent. A separate
claude -pcall on a small model, with no tools, no MCP servers and none of your customisations (--safe-mode), decides whether it is about your job search; the question sits between markers it cannot close, as data. A no, or a check that fails or answers outside its schema, stops it there: the check fails closed. The question also loses the characters a reader cannot see, and is capped at 2,000 characters and 30 an hour. - Claude Code has jobdork's tools and nothing else. Its built-in tools
are off (
--tools "": no shell, files or web), jobdork's MCP server is the only one (--strict-mcp-config), only its tools are allowed and anything else is refused without asking (--permission-mode dontAsk), and your own settings, hooks and plugins are not read (--setting-sources project, in an empty folder). Its system prompt is jobdork's, not Claude Code's, and has it refuse anything outside the job search. So a question that gets past the check still reaches only what jobdork's tools reach, under the limits above (For a coding agent). - The model is one of a fixed list. The page offers Sonnet, Opus, Haiku
or Claude Code's default; the server accepts only those names
(
ask.MODELS) and passes nothing else toclaude --model. The check stays on Haiku whichever answers. - Only the desktop app answers it. The server refuses
/api/askwithout the app (a browser on a Docker or server jobdork gets 404), and every request needs the dashboard's token, as all others do.
- A compromise of GitHub, Docker Hub or Sigstore themselves. Verifying the signature narrows this, but does not remove it.
- The machine you run it on. Anyone who can run commands there as your user can read jobdork's database and settings.
- The dashboard's access token. It is printed in the container's log and put in the link you open; treat that link as a password. See docs/run/nas.md for keeping the dashboard on your own network.