Tutorials / Native Mac IDE / Customize permissions and env vars in settings.json
📝 Written ● Intermediate Updated 2026-05-19

Customize permissions and env vars in settings.json

Permission prompts and environment variables aren't a UI toggle in LingCode — they live in a real file at ~/.claude/settings.json. Once you know where it is and what shape it has, you can stop clicking "approve" on ls for the fiftieth time today.

Why this file exists at all

0

By default LingCode prompts before every Bash command, every file write outside the project root, and every tool with side effects. That's the safe default — you don't want a fresh agent silently running rm -rf in your home directory.

But after using LingCode for a week you'll notice the same prompts pile up: ls, git status, cat package.json, npm test. None of those need approval. Each one costs 2–3 seconds and breaks your focus. Multiply by 50 a day.

The fix isn't a hidden menu — it's a JSON file you own and edit. Two locations:

  • ~/.claude/settings.json — your user settings. Applies to every project on this Mac.
  • <repo>/.claude/settings.local.jsonproject settings. Applies only inside that repo and overrides the user file.

Three top-level keys matter for this tutorial: permissions.allow, permissions.deny, and env. (There's a fourth — hooks — which has its own tutorial; see Add custom hooks.)

Find or create the file

1

In a terminal:

mkdir -p ~/.claude
touch ~/.claude/settings.json
open -e ~/.claude/settings.json

If the file is brand-new and empty, paste this skeleton and save:

{
  "permissions": {
    "allow": [],
    "deny": []
  },
  "env": {}
}

LingCode picks the file up on the next prompt — no restart needed.

The shape of a permission rule

2

Each entry in allow and deny is a string matching a tool call. Two common shapes:

  • "Bash(<command pattern>)" — match a shell command. The pattern supports a literal command or a prefix glob.
  • "<ToolName>" — match any invocation of that tool, regardless of arguments. Useful for read-only tools you trust unconditionally.

Example:

{
  "permissions": {
    "allow": [
      "Bash(ls:*)",
      "Bash(cat:*)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Bash(npm test:*)",
      "Read",
      "Glob",
      "Grep"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(sudo:*)"
    ]
  }
}

Bash(ls:*) allows any invocation starting with lsls, ls -la, ls src/. Bash(git status) is exact-match only.

Deny wins. If the same command matches both an allow and a deny rule, the deny rule takes effect. You can safely use broad allow patterns as long as your deny list covers the dangerous edges.

User file vs project file — which to edit

3

The two files merge, with the project file overriding the user file. Use this split:

  • User (~/.claude/settings.json): commands safe in any repo — ls, cat, git status, git diff, git log, pwd, which. These don't depend on the project.
  • Project (<repo>/.claude/settings.local.json): commands specific to this codebase — npm test, cargo build, ./scripts/build.sh, xcodebuild build. Different repos need different allowlists.

The project file should be gitignored if it contains anything reflecting your personal workflow. If you want to share a baseline allowlist with your team, commit a .claude/settings.json (without .local) at the repo root — LingCode reads that too, and project + local layer on top.

Environment variables

4

The env block sets variables that LingCode (and the commands it runs) sees. Useful for:

  • Provider keys per-project — different API key in this repo than your default.
  • Tool togglesDEBUG=1, NODE_ENV=development, NO_COLOR=1.
  • Build pathsJAVA_HOME, ANDROID_HOME if your shell rc doesn't already export them.
{
  "env": {
    "NODE_ENV": "development",
    "NO_COLOR": "1",
    "PYTHONDONTWRITEBYTECODE": "1"
  }
}
Don't put long-lived secrets here directly if the file is shared. Project .claude/settings.json is often committed; reach for the Keychain or shell rc instead. Project local settings (settings.local.json) are gitignored by default and are the right place for per-machine secrets.

Useful starter allowlists

5

Read-only inspection (safe everywhere)

"Bash(ls:*)",
"Bash(cat:*)",
"Bash(head:*)",
"Bash(tail:*)",
"Bash(pwd)",
"Bash(which:*)",
"Bash(file:*)",
"Bash(stat:*)"

Git read-only

"Bash(git status)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git branch:*)",
"Bash(git show:*)",
"Bash(git remote -v)"

Node / npm project

"Bash(npm test:*)",
"Bash(npm run lint:*)",
"Bash(npm run typecheck:*)",
"Bash(npx tsc --noEmit:*)",
"Bash(node --version)"

Swift / Xcode project

"Bash(swift build:*)",
"Bash(swift test:*)",
"Bash(xcodebuild build:*)",
"Bash(xcodebuild test:*)",
"Bash(xcrun simctl list)"

Drop these into permissions.allow for the project type that fits, then add to the list over the next few days as you notice approvals you keep granting.

The deny list — write it once, sleep better

6

The point of broad allow rules is they're forgiving. The point of deny rules is they're absolute. A short, high-confidence deny list:

"permissions": {
  "deny": [
    "Bash(rm -rf:*)",
    "Bash(rm -fr:*)",
    "Bash(sudo:*)",
    "Bash(curl * | sh)",
    "Bash(curl * | bash)",
    "Bash(:(){ :|:& };:)",
    "Bash(dd if=:*)",
    "Bash(mkfs:*)",
    "Bash(:>/dev/sda:*)"
  ]
}

Even with permissions.allow: ["Bash(*)"], the deny list still fires. This is the floor — set it once and forget it.

Verify the file is loading

7

After editing, ask LingCode in chat:

Run `git status` and tell me whether you were prompted.

If your allowlist took effect, LingCode runs the command and reports the output without an approval dialog. If you still get the prompt, two likely causes:

  • JSON is malformed. A trailing comma or missing brace silently drops the whole file. Run python3 -m json.tool ~/.claude/settings.json — it prints a parse error if so.
  • Pattern mismatch. "Bash(git status)" matches exactly git status; LingCode may have run git status --short. Switch to "Bash(git status:*)".
Watch for what LingCode actually ran. The approval dialog shows the exact command string LingCode wants to run — copy that, write the matching allow pattern, and the next identical request goes through silently.

Iterating without burning your day

8

Don't try to write the perfect allowlist upfront. The right workflow:

  1. Start with a small allowlist (read-only inspection + git status/diff).
  2. Use LingCode normally. When you find yourself approving the same command twice in a session, add it.
  3. After a week, you'll have a personalized allowlist that matches your actual workflow — not someone else's guess.

If you'd rather not do this by hand, there's a separate tutorial that scans your past transcripts and proposes an allowlist based on what you've actually approved: Reduce permission prompts by allowlisting safe commands.

Use this in LingCode

9

The whole workflow — locate, shape, scope user vs project, verify — is packaged as a skill. Drop it into your skills folder and ask LingCode for "configure settings.json" to invoke it:

---
name: update-config
description: Configure the Claude Code harness via settings.json — permissions allow/deny and env vars (NOT hooks; hooks have their own skill). Triggers: 'allow X command', 'add permission to allowlist', 'set DEBUG=true', 'environment variable for Claude Code', 'move permission to user settings', 'configure settings.json', 'permissions config'. Actions: edit ~/.claude/settings.json (user) or .claude/settings.local.json (project), append to permissions.allow / permissions.deny, set env keys. File shape: JSON with permissions, env, hooks. Skip for: theme/model switches (use /config command), hook setup (use add-custom-hooks skill).
---

Configure LingCode by editing settings.json directly. Settings live in
two places that merge:

- User file: ~/.claude/settings.json (applies to every project)
- Project file: <repo>/.claude/settings.local.json (this repo only;
  overrides user file)

For permission changes:
1. Open the relevant file. Create it with skeleton {permissions:
   {allow: [], deny: []}, env: {}} if it doesn't exist.
2. Identify the exact command string the user wants allowed or denied.
   Bash commands use "Bash(<pattern>)" — pattern is literal or
   prefix-glob with ":*".
3. Add to permissions.allow or permissions.deny. Deny always wins.
4. For tools other than Bash, use the tool name directly: "Read",
   "Glob", "Grep".

For env var changes:
1. Add to the env object as "KEY": "value" strings.
2. Long-lived secrets go in settings.local.json (gitignored), not the
   shared settings.json.

After editing, validate with `python3 -m json.tool` and ask LingCode
to run a sample command to confirm the rule took effect. If the
prompt still appears, the most likely cause is a glob mismatch — read
the dialog's exact command string and write the pattern to match it.

Scope user vs project deliberately: read-only universal commands (ls,
cat, git status) belong in the user file; project-specific build and
test commands belong in the project file.

Save as ~/.lingcode/skills/update-config/SKILL.md — see Install a skill for the exact location and how skills get discovered.

Get LingCode →

What's next