There is a tempting way to give an AI agent access to a GitHub repo.
Give it shell access. Put a GitHub token in the environment. Let it run git, gh, pnpm, npm, and whatever else it needs. Then tell it very clearly not to do anything bad.
That works, right up until it doesn’t.
I recently needed one specialized OpenClaw agent to work with one repo. Not every repo. Not my whole filesystem. Not arbitrary shell commands. One repo. The agent needed to inspect files, make edits, prepare commits, run a build, and preview a blog locally. It did not need to see the token. It did not need to read environment variables. It did not need general-purpose terminal access.
So I ended up with a pattern I like much better:
Do not give the agent the secret. Give the agent a capability.
The secret lives outside the agent workspace. A narrow wrapper command uses the secret internally. The agent can call that wrapper, and the wrapper only knows how to operate on one repo.
This is not about distrusting the agent. It is about not making every mistake catastrophic.
The problem with “just give it a token”
A fine-grained GitHub PAT is a good start. If you need an agent to work with one repo, the token should be scoped to that repo and only the permissions it needs.
But a repo-scoped PAT is not enough by itself.
If the agent also has arbitrary shell access, it can still do things you probably did not intend:
- print environment variables
- read token files
- inspect unrelated local files
- make unexpected network calls
- accidentally paste secrets into chat output
- commit generated files, caches, or private notes
- use the token in ways outside the workflow you had in mind
Even if the token is limited, the blast radius is still larger than it needs to be.
The safer model is capability design. Instead of handing the agent a credential and hoping it behaves, you give it a tool that already encodes the boundary.
The shape of the setup
The agent gets its own workspace:
/data/workspace-<agentId>
The repo is cloned inside that workspace:
/data/workspace-<agentId>/<repo-category>/<repo-name>
The token is stored outside all workspaces:
/data/.openclaw/secrets/<agent>-<repo>-github.env
That file is owned by root and locked down:
600 root:root
Then there is a persistent wrapper command:
/data/.openclaw/agent-tools/<agent>-<repo>
That wrapper is also root-owned:
755 root:root
The agent does not read the token file. It does not get the token in memory. It does not get to run printenv and see it. The wrapper sources the secret internally, exports GH_TOKEN or GITHUB_TOKEN only inside its own process, and then runs the specific Git or GitHub operation.
The wrapper hardcodes two things:
- the allowed GitHub repo slug
- the allowed local repo path
That hardcoding matters. It means the agent cannot casually point the same credential at some other repo or wander into another directory and start operating there.
What the wrapper exposes
The wrapper exposes repo-specific operations, not a general shell.
For normal Git work, commands like this are enough:
<wrapper> status
<wrapper> diff
<wrapper> log
<wrapper> branch
<wrapper> switch -c <branch>
<wrapper> add <files>
<wrapper> commit -m "message"
<wrapper> pull
<wrapper> push
For GitHub work, the wrapper can expose scoped gh commands:
<wrapper> gh pr ...
<wrapper> gh issue ...
<wrapper> gh run ...
<wrapper> gh workflow ...
<wrapper> gh release ...
The important part is the API restriction. gh api should only be allowed for the assigned repo:
repos/<owner>/<repo>
repos/<owner>/<repo>/...
Calls to anything else should fail inside the wrapper before they ever reach GitHub.
This gives the agent the useful part of GitHub access without turning the token into a general-purpose credential sitting in the agent’s lap.
Repo work usually needs more than Git
Git access was only the first half of the problem.
Once the agent could edit the repo, it needed to validate the work. In my case, the repo was an Astro site using pnpm. The agent needed to install dependencies, run the build, check formatting, run lint, and sometimes start a local dev server so I could inspect the change in a browser.
The unsafe fix would have been obvious:
just give the agent shell access
But that would have defeated the whole point.
So the wrapper grew a few exact repo-local commands:
<wrapper> install
<wrapper> build
<wrapper> lint
<wrapper> format-check
<wrapper> format
<wrapper> dev
<wrapper> preview
For the Astro/pnpm site, those map to commands like:
corepack pnpm install --frozen-lockfile --prod=false
corepack pnpm build
corepack pnpm lint
corepack pnpm format:check
corepack pnpm format
corepack pnpm dev -- --host 0.0.0.0
corepack pnpm preview -- --host 0.0.0.0
But the agent never gets pnpm directly. It never gets npm. It never gets node. It never gets bash.
It gets capabilities:
- install this one repo’s dependencies
- build this one repo
- lint this one repo
- check formatting for this one repo
- run this one repo’s dev server for a bounded amount of time
- preview this one repo for a bounded amount of time
That distinction matters. The wrapper decides where the command runs and exactly what it runs. The agent does not get to improvise a shell pipeline because something failed.
The small bug that proved the design
There was a funny bug in the first version of the build tooling.
The install command ran in a production-like environment, so pnpm skipped devDependencies. The build worked, but format-check failed because Prettier was not installed.
The fix was not to give the agent more power. The fix was to make the wrapper more precise:
NODE_ENV=development
pnpm install --frozen-lockfile --prod=false
After that, the toolchain was available and the command behaved correctly.
That is exactly the kind of failure mode I want. The wrapper was missing a capability detail. We fixed the wrapper. We did not expand the agent into a general-purpose system operator.
There was another useful outcome too: format-check executed successfully as a command, but reported existing formatting differences across the repo. That distinction is important. The tool worked. The repo just was not Prettier-clean yet.
So the operating rule became:
- agents can run
format-checkfreely - agents should not run broad
formatunless the user explicitly asks for a formatting pass - agents can run
buildbefore asking for approval to push or deploy
Again, the theme is the same: useful power, narrow boundary.
The OpenClaw tool policy
The second half of the design is the OpenClaw tool policy.
The agent can still have normal safe tools: filesystem access inside its workspace, web fetch/search if needed, image/PDF analysis if that fits the role. But exec should be allowlisted down to the wrapper only.
The generic shape looks like this:
{
"tools": {
"fs": {
"workspaceOnly": true
},
"exec": {
"host": "gateway",
"security": "allowlist",
"ask": "off",
"safeBins": [
"<wrapper-name>"
],
"pathPrepend": [
"/data/.openclaw/agent-tools"
],
"safeBinTrustedDirs": [
"/data/.openclaw/agent-tools"
],
"timeoutSec": 600
}
}
}
The details matter:
workspaceOnly: truekeeps normal file operations inside the agent workspace.safeBinscontains only the wrapper command.pathPrependpoints to the persistent helper directory.safeBinTrustedDirstells OpenClaw where trusted helper binaries live.- The agent does not get generic shell.
- The agent does not get process inspection.
- The agent does not get broad runtime/session/node/messaging tools unless it actually needs them.
The result is a small command surface that matches the job.
The agent can do useful repo work. It cannot casually inspect the host, dump env vars, read token files, or use the GitHub credential outside the wrapper’s rules.
The persistence trap
One lesson I learned the annoying way: putting the wrapper in /usr/bin is not enough.
Inside a containerized setup, /usr/bin is part of the container overlay. It may survive a normal process restart. It may even survive a gateway restart. But it is not the right source of truth if the container is recreated or updated.
The persistent place for agent helper tools should be under /data:
/data/.openclaw/agent-tools/<wrapper>
Then configure the agent’s exec policy to trust that directory:
{
"pathPrepend": [
"/data/.openclaw/agent-tools"
],
"safeBinTrustedDirs": [
"/data/.openclaw/agent-tools"
]
}
That should survive gateway restarts, container restarts, container recreation, and app updates, as long as /data is preserved.
There are still dependency caveats. The wrapper needs whatever binaries it calls. For GitHub work, that means git and gh. For an Astro/pnpm site, that also means Node, Corepack, and a timeout utility for bounded dev/preview commands.
If a future container image removes one of those, the answer is still not broad shell. The answer is to restore the dependency or rewrite the wrapper to use a different implementation.
Validation is part of the feature
A pattern like this is only useful if you actually test the boundary.
These are the checks I care about:
openclaw config validate
The config should parse before restart.
The persistent helper should exist:
/data/.openclaw/agent-tools/<wrapper>
The secret should exist outside the workspace with tight permissions:
/data/.openclaw/secrets/<secret-file>
600 root:root
The wrapper should work for the allowed repo:
<wrapper> status
<wrapper> gh api repos/<owner>/<repo> --jq ".default_branch"
The wrapper should fail for other repos:
<wrapper> gh api repos/<owner>/<other-repo>
The repo-local commands should work too:
<wrapper> install
<wrapper> build
<wrapper> format-check
A failing format check is not necessarily a wrapper failure. It may simply mean the repo has formatting differences. That is still useful signal.
The git remote should not contain a token:
git remote -v
You want a normal HTTPS remote URL, not a tokenized one.
And finally, search the relevant workspaces and memory files for token-like strings. The expected result is boring: nothing found.
That last check is worth doing. Secrets usually leak in boring ways. A copied terminal output. A debug note. A pasted config. A memory file that tried to be helpful. If the pattern is working, the agent’s workspace should contain repo code and working notes, not credentials.
Config editing gotcha
One more practical detail: some OpenClaw config paths are protected.
When I tried to patch agent tool permissions through the first-class config patch flow, protected paths like these were rejected:
agents.list[].tools.allow
agents.list[].tools.deny
agents.list[].tools.exec.*
That is a reasonable safety feature. Tool policy changes are sensitive.
The fallback was the old-fashioned careful path:
- make a timestamped backup of
/data/.openclaw/openclaw.json - edit the JSON carefully
- validate with
openclaw config validate - restart OpenClaw
- verify the wrapper still works
The important mistake to avoid is round-tripping redacted config through a tool that might write it back. If a config dump has secrets replaced with placeholder text, do not apply that dump as if it were the real config. Redacted values can overwrite real secrets.
The reusable pattern
For the next agent that needs repo access, the recipe is straightforward:
- Create an isolated top-level workspace.
- Keep filesystem access workspace-only.
- Store the repo-scoped PAT outside all workspaces.
- Create a persistent root-owned wrapper under
/data/.openclaw/agent-tools/. - Hardcode the allowed repo and local repo path in the wrapper.
- Clone the repo under the agent workspace.
- Allow
execonly for that wrapper. - Add exact repo-local build/test/dev commands if the agent needs them.
- Deny generic shell and process inspection.
- Validate that allowed repo operations work.
- Validate that other repo operations fail.
- Validate that build/check commands work.
- Confirm the token is not in memory, workspace files, logs, or git remotes.
The principle is simple: make the safe path the easy path.
An agent should not need to remember not to leak a token it never received. It should not need to resist reading a secret file it cannot access. It should not need to decide which repo is allowed if the wrapper already encodes that boundary.
Why this matters
Good agent design should assume reality.
Prompts can go sideways. Tools can be misused. Logs can get copied. Config survives longer than memory. Container filesystems are not always persistent. A well-meaning agent can still make a mess if the system gives it too much room.
The answer is not to make agents useless. The answer is to give them narrow, durable capabilities that match the job.
For GitHub access, that means wrappers, not raw tokens. Repo-specific commands, not generic shell. Workspace-only filesystem, not a tour of the host. Exact build and preview commands, not arbitrary package-manager access. Explicit approval before pushing or publishing, not silent live changes.
That is the balance I want for my own OpenClaw agents: useful enough to do real work, constrained enough that a mistake stays small.