mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
* security(skills): drop bare Bash and the portal CLI glob from skill grants A skill's allowed-tools pre-approves the listed tools for the turn that invokes it, even in an untrusted folder. job-application-assistant granted bare Bash, Edit and Write; job-scraper granted bun run on any .agents/skills/*/cli/src/cli.ts. Every Bash command the skills document is already allowlisted per command in .claude/settings.json, so the bare and glob grants bought nothing but exposure. The per-portal grants stay in settings.json, which is where a portal added by /add-portal registers too (Step 5 now says so). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * security(settings): deny exfil-shaped shell and credential reads, always ask on permission-defining files Deny rules block in every permission mode including bypassPermissions and override any allow rule or skill grant; ask rules force a prompt even when a skill's allowed-tools would pre-approve the call. settings.json now ships both: deny for shell that uploads or opens a remote shell (curl data/form/upload flags, wget post, scp, sftp, ssh, nc) and for reads of .env and ~/.ssh, ~/.aws, ~/.gnupg; ask for the files whose content is the permission surface (settings, skill frontmatter, agents, CI, this guard) and for git push. security_guards.py requires each rule to stay present, the same way it requires the personal-data gitignore rules. Prefix rules are tripwires, not walls; SECURITY.md will say so. Also allowlists tools/robots_check.py and the upcoming tools/fetch_page.py so the 403 retry no longer needs a curl grant. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * security(guards): hold skill and command allowed-tools to a reviewed allowlist The guard allowlisted settings.json permissions but never read skill frontmatter, so the narrow settings allowlist sat next to a bare Bash grant it could not see. Check 4 parses every .claude/skills/*/SKILL.md, .agents/skills/*/SKILL.md and .claude/commands/*.md frontmatter with a stdlib parser (scalar, folded, and list forms) and holds each allowed-tools entry to ALLOWED_SKILL_TOOLS; a portal skill may additionally grant exactly its own cli/src/cli.ts, matched by path so no portal name is hardcoded. Bare Bash and mid-pattern wildcards get their own message. On master's frontmatter this check reports two failures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * feat(tools): add GET-only fetch_page.py for the browser-header retry 09-web-research.md documented the 403 retry as a curl command with browser headers plus an inline python3 -c tag-stripper, which meant the workflow wanted a Bash(curl:*) grant - and curl with a free tail is an upload channel. fetch_page.py is the same retry as one command with a fixed GET argv (URL after --, bounded to 45 s / 5 redirects / 10 MB), http(s) only, robots_check.gate() enforced with no bypass flag, --out refused inside the repo, and --text for the stripped page. urllib fallback when curl is absent. The settings.json grant names the script. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(security): describe the permission surface as a table; route the 403 retry through fetch_page.py SECURITY.md now lists every shipped grant with its location, lifetime and guard, and says plainly what the permission layer does not do: no sandbox on native Windows, prefix rules are tripwires, WebFetch is unrestricted by domain, subagents run with the parent's tools (next change), and Edit/Write stay granted because tailoring a CV is editing files. 09-web-research.md's retry section is the one fetch_page.py command (framework_version 1.1.3 -> 1.1.4); apply, rank and scrape point at it instead of curl. SETUP.md and the README tools tree follow. CHANGELOG gains a Security section under [Unreleased]. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * security(review): ask on command frontmatter, portal skills grant only their CLI, bound the urllib fallback Findings from a fresh-context review of the branch: - A command's frontmatter is a grant too (check 4 already parses it), so Edit(.claude/commands/*.md) joins the required ask rules; without it a steered session could write `allowed-tools: Bash` into apply.md with no prompt and the next /apply would be pre-approved for bare Bash. - SECURITY.md promised a portal skill may grant "its own CLI and nothing else"; the guard now enforces exactly that instead of own-CLI plus the core allowlist. All six shipped portal skills already comply. - fetch_page.py's urllib fallback now matches curl's 5-redirect cap and applies the 10 MB cap after decompression, so the docstring's bounds hold on both paths. - Frontmatter parser: a BOM no longer reads as "missing frontmatter", YAML flow lists split correctly, and a non-UTF-8 file fails with a message instead of a traceback. Module docstring lists check 4. - CHANGELOG no longer claims "no new prompt": compile and aux cleanup, which the removed bare Bash covered in a skill-invoking turn, now prompt, deliberately. "Once per session" wording corrected to per call. - Three upload-shaped deny siblings added (curl -XPOST, curl --request POST, wget --method=POST); /add-portal recon points at fetch_page.py. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
65 lines
2.1 KiB
JSON
65 lines
2.1 KiB
JSON
{
|
|
"permissions": {
|
|
"allow": [
|
|
"Skill(job-application-assistant)",
|
|
"Bash(bun run .agents/skills/jobbank-search/cli/src/cli.ts:*)",
|
|
"Bash(bun run .agents/skills/jobdanmark-search/cli/src/cli.ts:*)",
|
|
"Bash(bun run .agents/skills/jobindex-search/cli/src/cli.ts:*)",
|
|
"Bash(bun run .agents/skills/jobnet-search/cli/src/cli.ts:*)",
|
|
"Bash(bun run .agents/skills/linkedin-search/cli/src/cli.ts:*)",
|
|
"Bash(bun run .agents/skills/freehire-search/cli/src/cli.ts:*)",
|
|
"Bash(python salary_lookup.py:*)",
|
|
"Bash(python3 salary_lookup.py:*)",
|
|
"Bash(python tools/rank_state.py:*)",
|
|
"Bash(python3 tools/rank_state.py:*)",
|
|
"Bash(python tools/job_key.py:*)",
|
|
"Bash(python3 tools/job_key.py:*)",
|
|
"Bash(python tools/verify_pdf.py:*)",
|
|
"Bash(python3 tools/verify_pdf.py:*)",
|
|
"Bash(python tools/verify_layout.py:*)",
|
|
"Bash(python3 tools/verify_layout.py:*)",
|
|
"Bash(pdftotext:*)",
|
|
"Bash(python tools/robots_check.py:*)",
|
|
"Bash(python3 tools/robots_check.py:*)",
|
|
"Bash(python tools/fetch_page.py:*)",
|
|
"Bash(python3 tools/fetch_page.py:*)"
|
|
],
|
|
"ask": [
|
|
"Edit(.claude/settings.json)",
|
|
"Edit(.claude/settings.local.json)",
|
|
"Edit(.claude/skills/*/SKILL.md)",
|
|
"Edit(.claude/commands/*.md)",
|
|
"Edit(.claude/agents/**)",
|
|
"Edit(.github/**)",
|
|
"Edit(tools/security_guards.py)",
|
|
"Bash(git push:*)"
|
|
],
|
|
"deny": [
|
|
"Read(.env)",
|
|
"Read(.env.*)",
|
|
"Read(~/.ssh/**)",
|
|
"Read(~/.aws/**)",
|
|
"Read(~/.gnupg/**)",
|
|
"Bash(curl -d:*)",
|
|
"Bash(curl --data:*)",
|
|
"Bash(curl -F:*)",
|
|
"Bash(curl --form:*)",
|
|
"Bash(curl -T:*)",
|
|
"Bash(curl --upload-file:*)",
|
|
"Bash(curl -X POST:*)",
|
|
"Bash(curl -X PUT:*)",
|
|
"Bash(curl -XPOST:*)",
|
|
"Bash(curl --request POST:*)",
|
|
"Bash(wget --post-data:*)",
|
|
"Bash(wget --post-file:*)",
|
|
"Bash(wget --method=POST:*)",
|
|
"Bash(scp:*)",
|
|
"Bash(sftp:*)",
|
|
"Bash(ssh:*)",
|
|
"Bash(nc:*)",
|
|
"Bash(ncat:*)",
|
|
"Bash(netcat:*)"
|
|
]
|
|
}
|
|
}
|