# How the Security Guards CI Job Validates Bash Command Permissions in ai-job-search

> Learn how the security guards CI job validates bash command permissions in ai-job-search by comparing settings.json against an ALLOWED_PERMISSIONS whitelist to prevent unauthorized commands.

- Repository: [Mads Lorentzen/ai-job-search](https://github.com/MadsLorentzen/ai-job-search)
- Tags: how-to-guide
- Published: 2026-08-26

---

**The security guards CI job validates bash command permissions by comparing entries in [`.claude/settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/settings.json) against a static `ALLOWED_PERMISSIONS` whitelist defined in [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py), failing the build if any unauthorized command pattern is detected.**

The **MadsLorentzen/ai-job-search** repository uses Claude Code for AI-assisted development, requiring strict controls over which Bash commands can execute without user prompts. The **security guards CI job validates bash command permissions** by cross-referencing runtime configurations against a static whitelist before allowing any merge.

## CI Workflow Configuration

The validation process begins in [`.github/workflows/ci.yml`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.github/workflows/ci.yml) at lines 54–63, where the **security-guards** job is defined. This job checks out the repository, configures Python 3.12, and executes the validation script.

```yaml

# .github/workflows/ci.yml

security-guards:
  name: Security guards (permissions, gitignore, manifests)
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: "3.12"
    - run: python tools/security_guards.py

```

## The Permission Allow-List

Inside [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py), the constant `ALLOWED_PERMISSIONS` (lines 37–47) defines the complete set of pre-approved Bash command patterns and skills that may run without prompting. This static whitelist acts as the source of truth for all automated permission grants.

```python

# tools/security_guards.py

ALLOWED_PERMISSIONS = {
    "Skill(job-application-assistant)",
    "Bash(bun run:*)",
    "Bash(python salary_lookup.py:*)",
    "Bash(python3 salary_lookup.py:*)",
    "Bash(python tools/verify_pdf.py:*)",
    "Bash(python3 tools/verify_pdf.py:*)",
    "Bash(pdftotext:*)",
}

```

Each entry follows the pattern `Bash(command:*)` or `Skill(name)`, where wildcards allow argument variations while restricting the base command execution.

## Validation Logic in check_permissions()

The `check_permissions()` function (lines 49–99) performs the actual validation by reading [`.claude/settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/settings.json) and verifying each entry in the `permissions.allow` array against the whitelist.

The function extracts the permissions array and iterates through every entry:

```python

# tools/security_guards.py – core check

def check_permissions() -> None:
    path = ROOT / ".claude" / "settings.json"
    data = json.loads(path.read_text(encoding="utf-8"))
    allow = data.get("permissions", {}).get("allow", [])
    for entry in allow:
        if entry not in ALLOWED_PERMISSIONS:
            errors.append(
                f".claude/settings.json: permission not in the reviewed allowlist: {entry!r}. "
                "Pre‑approved permissions run without prompting on every fork. "
                "If this entry is intentional, add it to ALLOWED_PERMISSIONS in "
                "tools/security_guards.py in the same PR."
            )

```

If a permission entry is not found in `ALLOWED_PERMISSIONS`, the function appends an error message to the `errors` list, causing the CI job to fail and block the merge. Conversely, if the whitelist contains entries absent from [`settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/settings.json), the script prints a non-fatal note (lines 95–99) indicating stale allow-list items.

## How to Add and Validate New Bash Command Permissions

To authorize a new Bash command, you must update both the configuration file and the Python whitelist in the same pull request. For example, to add [`scripts/cleanup.sh`](https://github.com/MadsLorentzen/ai-job-search/blob/main/scripts/cleanup.sh):

First, update [`.claude/settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/settings.json):

```json
{
  "permissions": {
    "allow": [
      "Skill(job-application-assistant)",
      "Bash(bun run:*)",
      "Bash(python salary_lookup.py:*)",
      "Bash(python3 salary_lookup.py:*)",
      "Bash(python tools/verify_pdf.py:*)",
      "Bash(python3 tools/verify_pdf.py:*)",
      "Bash(pdftotext:*)",
      "Bash(scripts/cleanup.sh:*)"
    ]
  }
}

```

Then, add the pattern to `ALLOWED_PERMISSIONS` in [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py):

```python
ALLOWED_PERMISSIONS = {
    "Skill(job-application-assistant)",
    "Bash(bun run:*)",
    "Bash(python salary_lookup.py:*)",
    "Bash(python3 salary_lookup.py:*)",
    "Bash(python tools/verify_pdf.py:*)",
    "Bash(python3 tools/verify_pdf.py:*)",
    "Bash(pdftotext:*)",
    "Bash(scripts/cleanup.sh:*)",   # ← newly added entry

}

```

When the CI security-guards job re-runs, it detects the matching entries in both locations and passes validation.

## Summary

- The **security guards CI job** runs automatically on every pull request via [`.github/workflows/ci.yml`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.github/workflows/ci.yml).
- It executes [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py) to validate that all permissions in [`.claude/settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/settings.json) appear in the static `ALLOWED_PERMISSIONS` whitelist.
- **Unauthorized commands** trigger immediate CI failures, preventing merges until the whitelist is updated.
- **Dual updates** are required: both [`settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/settings.json) and [`security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/security_guards.py) must be modified in the same PR to authorize new Bash commands.
- This architecture ensures that every auto-approved command undergoes code review before deployment to forked repositories.

## Frequently Asked Questions

### What triggers a failure in the security guards CI job?

The CI job fails when [`.claude/settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/settings.json) contains a permission entry not present in the `ALLOWED_PERMISSIONS` set defined in [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py). The `check_permissions()` function appends an error to the validation log, causing the Python script to exit with a failure status that blocks the GitHub Actions workflow.

### Can I add wildcards to Bash command permissions?

Yes, the whitelist supports wildcard patterns such as `Bash(bun run:*)` and `Bash(python salary_lookup.py:*)`. These patterns allow variable arguments while restricting execution to specific base commands, enabling flexibility without compromising security boundaries.

### Why must I update both settings.json and security_guards.py?

The repository enforces a **two-source verification** model to ensure all auto-approved commands undergo explicit review. Updating only [`settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/settings.json) causes the CI job to fail because the new permission appears unauthorized. This design prevents accidental privilege escalation and guarantees that every approved command is tracked in version-controlled source code.

### Where does the CI job get the list of allowed permissions?

The CI job derives allowed permissions from the `ALLOWED_PERMISSIONS` constant in [`tools/security_guards.py`](https://github.com/MadsLorentzen/ai-job-search/blob/main/tools/security_guards.py) (lines 37–47). This Python set contains the master whitelist that the `check_permissions()` function compares against the runtime configuration stored in [`.claude/settings.json`](https://github.com/MadsLorentzen/ai-job-search/blob/main/.claude/settings.json).