{
  "id": "cursor-cli",
  "title": "Cursor CLI Cookbook",
  "lede": "Recipes for the `agent` binary: a permissions model with real glob patterns, a print mode that infers itself when you pipe, and shell mode for when you just want to run the command yourself.",
  "updated": "2026-07-12",
  "categories": [
    { "id": "permissions", "label": "Permissions" },
    { "id": "headless", "label": "Headless & CI" },
    { "id": "interactive", "label": "In the session" }
  ],
  "recipes": [
    {
      "id": "deny-secrets",
      "title": "Make the agent structurally unable to read your secrets",
      "category": "permissions",
      "difficulty": "starter",
      "when": "You want `.env`, keys, and credentials off the table entirely — not \"the model was told not to\".",
      "sheetRefs": ["permissions", "config"],
      "steps": [
        {
          "text": "Deny by pattern. The rules take globs, so you can name whole shapes of file rather than enumerating them.",
          "code": {
            "lang": "json",
            "file": ".cursor/cli.json",
            "text": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Shell(git)\",\n      \"Shell(npm run test)\",\n      \"Read(src/**/*.ts)\"\n    ],\n    \"deny\": [\n      \"Read(.env*)\",\n      \"Write(**/*.key)\",\n      \"Shell(rm)\"\n    ]\n  }\n}"
          }
        }
      ],
      "why": "A `deny` on `Read(.env*)` beats any instruction in a prompt, because it holds even when the agent has a *good reason* to look — a failing test that needs the database URL is exactly the moment a polite \"don't read secrets\" gets rationalized away.",
      "links": [
        { "label": "Permissions", "url": "https://cursor.com/docs/cli/reference/permissions" }
      ]
    },
    {
      "id": "scoped-ci-agent",
      "title": "Give a CI agent exactly one job",
      "category": "permissions",
      "difficulty": "intermediate",
      "when": "An agent runs on every PR and should comment — but never push, never branch, never merge.",
      "sheetRefs": ["permissions", "headless"],
      "steps": [
        {
          "text": "State the boundary in the prompt *and* enforce it in the config. The prompt tells it what you want; the deny list is what holds when the prompt doesn't.",
          "code": {
            "lang": "bash",
            "text": "agent -p \"Review the diff and post findings as a PR comment.\nIMPORTANT: Do NOT create branches, commit, push, or merge.\" \\\n  --model gpt-5"
          }
        },
        {
          "text": "Back it with a config the prompt can't talk its way past.",
          "code": {
            "lang": "json",
            "file": ".cursor/cli.json",
            "text": "{\n  \"permissions\": {\n    \"allow\": [\"Shell(gh pr comment)\", \"Shell(git diff)\", \"Shell(git log)\"],\n    \"deny\": [\"Shell(git push)\", \"Shell(git commit)\", \"Shell(gh pr merge)\"]\n  }\n}"
          }
        }
      ],
      "why": "Two layers, because they fail differently. The prompt shapes intent and produces better work; the deny list is what's left standing when the model decides pushing a fix would be *helpful*.",
      "links": [
        { "label": "GitHub Actions", "url": "https://cursor.com/docs/cli/github-actions" }
      ]
    },
    {
      "id": "piping-agent",
      "title": "Pipe the agent without remembering a flag",
      "category": "headless",
      "difficulty": "starter",
      "when": "You want the agent's answer in a shell variable or another process.",
      "sheetRefs": ["headless", "flags"],
      "steps": [
        {
          "text": "Print mode is **inferred** when stdout isn't a TTY — piping is enough, `-p` is belt and braces.",
          "code": {
            "lang": "bash",
            "text": "agent -p \"List every TODO in src/ with its file and line.\" > todos.txt"
          }
        },
        {
          "text": "For machine-readable output, ask for JSON explicitly. `--output-format` is only valid in print mode.",
          "code": {
            "lang": "bash",
            "text": "agent -p \"Is the build config valid? Answer yes or no.\" \\\n  --output-format json | jq -r '.result'"
          }
        },
        {
          "text": "`stream-json` emits NDJSON — one line per assistant message — so you can act on progress instead of waiting.",
          "code": {
            "lang": "bash",
            "text": "agent -p \"Refactor the auth module.\" --output-format stream-json \\\n  | while read -r line; do echo \"$line\" | jq -r '.type'; done"
          }
        }
      ],
      "why": "The inference is a nice touch but it cuts both ways: a script that works interactively can silently change behavior when its stdout gets redirected. If you depend on the format, name it.",
      "links": [
        { "label": "Output format", "url": "https://cursor.com/docs/cli/reference/output-format" }
      ]
    },
    {
      "id": "shell-mode",
      "title": "Run the command yourself, mid-conversation",
      "category": "interactive",
      "difficulty": "starter",
      "when": "You need to check something — the test output, the branch, the file listing — without dropping the thread you're in.",
      "sheetRefs": ["shell", "keys"],
      "steps": [
        {
          "text": "Shell mode runs in your login shell (`$SHELL`) with the CLI's working directory. It's a real shell, so chain when you need to move.",
          "code": {
            "lang": "bash",
            "text": "cd subdir && npm test"
          }
        },
        {
          "text": "Keep the commands non-interactive. A prompt-for-input command will just hang — cancel with Ctrl+C and add the non-interactive flag.",
          "code": {
            "lang": "bash",
            "text": "npm install --yes        # not: npm install (which may prompt)\ngit rebase --continue    # not: git rebase -i"
          }
        }
      ],
      "why": "The output lands in the conversation, so the agent sees what you saw. That's the point — it beats copy-pasting a stack trace back in, and it keeps the agent's picture of the repo honest.",
      "links": [
        { "label": "Shell mode", "url": "https://cursor.com/docs/cli/shell-mode" }
      ]
    },
    {
      "id": "resume-work",
      "title": "Continue where you stopped",
      "category": "interactive",
      "difficulty": "starter",
      "when": "The task outlived the session and you don't want to re-explain it.",
      "sheetRefs": ["launch", "flags"],
      "steps": [
        {
          "text": "Resume the previous conversation rather than starting cold.",
          "code": {
            "lang": "bash",
            "text": "agent --continue"
          }
        }
      ],
      "why": "Re-explaining context isn't just slow, it's lossy — you'll summarize away the detail that turns out to matter. Resuming keeps the agent's working memory of the codebase intact.",
      "links": [
        { "label": "CLI parameters", "url": "https://cursor.com/docs/cli/reference/parameters" }
      ]
    }
  ]
}
