mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
security: narrow skill grants, ship ask/deny rules, guard skill frontmatter, GET-only 403 retry (#642)
* 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>
This commit is contained in:
1 parent
0f6480d32e
commit
01a3c0e5f6
15 files changed
+1059
-47
No files matched your search
@@ -498,6 +498,58 @@ per-file diff commands.
|
||||
|
||||
- **Salary lookups preserve Unicode company identities and match ASCII-typed accented names such as `Nestle` to `Nestlé`, while retaining Nordic spelling variants** (#570).
|
||||
|
||||
### Security
|
||||
|
||||
- **Skill frontmatter no longer grants bare `Bash` or a portal-CLI glob**
|
||||
(`.claude/skills/job-application-assistant/SKILL.md` `framework_version: 1.3.4 -> 1.3.5`,
|
||||
`.claude/skills/job-scraper/SKILL.md`, `.claude/commands/add-portal.md` Step 5) - a skill's
|
||||
`allowed-tools` pre-approves the listed tools for the turn that invokes it, with no prompt and
|
||||
regardless of workspace trust. The job-application skill granted `Bash`, `Edit`, `Write`
|
||||
outright, and the scraper granted `bun run` on any `.agents/skills/*/cli/src/cli.ts`, while
|
||||
`.claude/settings.json` carefully enumerated six portals - a public review by an AI agent
|
||||
(shared on Reddit) read exactly that and concluded local use was unsafe as configured. The
|
||||
tools scripts, salary lookup, `pdftotext` and the portal CLIs were already allowlisted per
|
||||
command in `settings.json`, so no new standing grant is needed. What the bare grant did cover,
|
||||
in a turn where the skill fired, was compile (`lualatex`/`xelatex`) and the `rm -f` of build
|
||||
intermediates; those now prompt in default mode, as the SECURITY.md table says - a
|
||||
`lualatex:*` grant would also approve `-shell-escape`, so they are deliberately not
|
||||
pre-approved. The per-portal grants stay in `settings.json`; `/add-portal` now says a new
|
||||
portal is pre-approved only once its own entry is there and in `ALLOWED_PERMISSIONS`.
|
||||
|
||||
- **`.claude/settings.json` ships `ask` and `deny` rules, and the guard requires them**
|
||||
(`tools/security_guards.py` `REQUIRED_ASK_RULES` / `REQUIRED_DENY_RULES`,
|
||||
`tests/test_security_guards.py`) - 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. Denied: `curl`/`wget` with data,
|
||||
form or upload flags, `scp`, `sftp`, `ssh`, `nc`, and reads of `.env`, `.env.*`, `~/.ssh/**`,
|
||||
`~/.aws/**`, `~/.gnupg/**`. Always asked: edits to `settings.json`, `settings.local.json`,
|
||||
`.claude/skills/*/SKILL.md`, `.claude/agents/**`, `.github/**`, `tools/security_guards.py`, and
|
||||
`git push` - the files whose content is the permission surface cannot be widened silently from
|
||||
inside a session. Prefix rules are tripwires, not walls; `SECURITY.md` says so.
|
||||
|
||||
- **The security guard now reads skill and command frontmatter** (`tools/security_guards.py`
|
||||
check 4, `ALLOWED_SKILL_TOOLS`) - it allowlisted `settings.json` permissions and hooks but never
|
||||
looked at `allowed-tools`, so the narrow settings allowlist sat next to a bare `Bash` grant it
|
||||
could not see. Every `.claude/skills/*/SKILL.md`, `.agents/skills/*/SKILL.md` and
|
||||
`.claude/commands/*.md` frontmatter is now held to an exact allowlist; a portal skill may
|
||||
additionally grant exactly its own `cli/src/cli.ts`, matched by path so no portal name is
|
||||
hardcoded. Bare tools and mid-pattern wildcards fail CI with their own message. On the previous
|
||||
frontmatter this check reports two failures.
|
||||
|
||||
- **The browser-header 403 retry is `python3 tools/fetch_page.py`, not a documented `curl`**
|
||||
(`tools/fetch_page.py`, `tests/test_fetch_page.py`,
|
||||
`.claude/skills/job-application-assistant/09-web-research.md` `framework_version: 1.1.3 -> 1.1.4`,
|
||||
`.claude/commands/apply.md` Step 0 and Step 3, `.claude/commands/rank.md` Step 2,
|
||||
`.claude/skills/job-scraper/SKILL.md` Step 2) - the retry was a curl command 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. The script is one GET with a fixed argv (URL after `--`,
|
||||
45 s, 5 redirects, 10 MB), http(s) only, runs `robots_check.gate()` first with no bypass flag,
|
||||
refuses an `--out` inside the repo, and prints the stripped text with `--text`. The grant names
|
||||
the script (`Bash(python tools/fetch_page.py:*)` and `robots_check.py` likewise). `SECURITY.md`
|
||||
now lists every shipped grant, its lifetime and its guard in one table, and states what the
|
||||
permission layer does not do (no sandbox on native Windows, WebFetch unrestricted by domain,
|
||||
subagents unrestricted - the latter is the next change).
|
||||
|
||||
## [1.7.2] - 2026-09-29
|
||||
|
||||
### Added
|
||||
|
||||
Reference in new issue
Block a user