SecurityCommercialFreeActiveMachine-verified· intermediate · ~5 min setup

Claude Code: Lock Down an Unattended Run with Permission Rules

Define exactly what a scheduled or headless Claude Code run may do via settings.json permission rules.

by Shilpa Mitra· verified today· v1.0.0

Run this workflow

CI-verified, 5/5 fixtures passing.

Intended Use

Defining exactly what an unattended Claude Code run may do via settings.json permission rules, so a scheduled or headless run cannot do something you would regret.

Not for

  • Granting blanket access, the point is least privilege
  • bypassPermissions environments, which skip the permission layer entirely
  • Trusting allow rules alone, deny always wins and should cover pushes, deletes, and secrets

The Stack

Tested Against

claude-code@2.1.83node@20.x

Side effects & data flow

Network
none, local only
Writes
./settings.json, ./checkperms.mjs
Credentials
none required

Steps

  1. 1

    Write the permission rules

    Permission rules live in settings.json under permissions, with allow, deny, and ask lists plus a defaultMode. dontAsk mode runs only what your allow list permits and silently denies the rest, which is exactly what you want for a scheduled or headless run.

    cat > settings.json <<'EOF'
    {
      "permissions": {
        "defaultMode": "dontAsk",
        "allow": [
          "Read",
          "Bash(npm test)",
          "Bash(npm run lint)",
          "Bash(git status)",
          "Bash(git diff *)",
          "WebFetch(domain:docs.python.org)"
        ],
        "deny": [
          "Bash(git push *)",
          "Bash(rm *)",
          "Read(.env)",
          "Edit(.env)",
          "Edit(/secrets/**)"
        ]
      }
    }
    EOF
    echo "wrote settings.json"
  2. 2

    What CI checks: the JSON parses and the deny list actually blocks the dangerous things

    CI parses the JSON, confirms defaultMode is a real mode (dontAsk, per code.claude.com/docs), checks the deny list blocks pushes, deletions, and secret files, and checks the allow list contains only scoped entries (no blanket Bash(*)). No API key needed.

    cat > checkperms.mjs <<'EOF'
    import { readFileSync } from 'node:fs';
    const cfg = JSON.parse(readFileSync('settings.json','utf8'));
    const MODES = ['default','acceptEdits','plan','auto','dontAsk','bypassPermissions'];
    const p = cfg.permissions || {};
    const deny = p.deny || [];
    const allow = p.allow || [];
    let ok = true;
    function check(label, cond){ console.log(label + ': ' + (cond ? 'yes' : 'NO')); if(!cond) ok=false; }
    check('settings.json parses', true);
    check('defaultMode is a real mode', MODES.includes(p.defaultMode));
    check('deny blocks git push', deny.some(r => r.includes('git push')));
    check('deny blocks rm', deny.some(r => r.includes('rm')));
    check('deny blocks .env', deny.some(r => r.includes('.env')));
    const dangerous = a => a === 'Bash(*)' || a.includes('rm ') || a.includes('git push');
    check('allow is scoped (no blanket Bash(*) or destructive)', allow.length > 0 && !allow.some(dangerous));
    if(!ok){ console.log('permission lockdown check FAILED'); process.exit(1); }
    console.log('permission lockdown check OK');
    EOF
    node checkperms.mjs
  3. 3

    Run something with it (the model step, not checked by CI)

    A run that uses these rules invokes the model, which is non-deterministic and fenced. CI proves the rules are well-formed and block the dangerous operations; it does not run the agent.

Eval, 5 fixtures

Last passed: verified today
  • mode-validcontainstimeout 30s · max $0

    Expected: defaultMode is a real mode: yes

  • deny-pushcontainstimeout 30s · max $0

    Expected: deny blocks git push: yes

  • deny-secretscontainstimeout 30s · max $0

    Expected: deny blocks .env: yes

  • check-okcontainstimeout 30s · max $0

    Expected: permission lockdown check OK

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 0

Results

Rules evaluate deny, then ask, then allow, so a deny always wins. Verifiable with no API key.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).