Extend, Revoke, and Alternatives for Pi-hole Access
Apart from Revoke Access, everything on this page is optional.
Start at Give Claude Code Access to Pi-hole if you haven’t set up access yet.
Optional: Give Claude Code Write Access
Section titled “Optional: Give Claude Code Write Access”Follow this section if you want Claude Code to apply fixes itself rather than suggest them to you.
Don’t follow this section if you want an agent that only reports.
Granting read access to one file, /etc/pihole/cli_pw, lets Claude Code run pihole allow, pihole deny, pihole enable, and pihole disable with no sudo involved at all.
/etc/pihole/cli_pw holds the password Pi-hole generates for its own pihole commands to authenticate against the API.
It also gives it more than those four commands.
Those four commands are the intended use, but the grant hands over a credential, not a command list. Anything that credential can do, Claude Code can do by calling the API directly.
In practice that means it can also rebuild gravity, restart DNS, delete DHCP leases and network device records, and flush the query log, which destroys the history you granted read access to in the first place.
It can remove lists as well as add them, and pihole disable turns off blocking for your whole network.
-
Select where you run the commands from:
And who you run the commands as:
From the Pi sudo setfacl -m "u:${USER}:r" /etc/pihole/cli_pwFrom the Pi sudo setfacl -m u:claude-agent:r /etc/pihole/cli_pwReplace
pi-adminwith your own account name on the Pi:From your workstation ssh -t pi-hole-admin 'sudo setfacl -m u:pi-admin:r /etc/pihole/cli_pw'From your workstation ssh -t pi-hole-admin 'sudo setfacl -m u:claude-agent:r /etc/pihole/cli_pw' -
Confirm it works by allowing a domain as that account, with no
sudo:From the Pi pihole allow ads.example.comFrom the Pi sudo -u claude-agent pihole allow ads.example.comFrom your workstation ssh pi-hole-admin 'pihole allow ads.example.com'From your workstation ssh pi-hole-agent 'pihole allow ads.example.com' -
Uncomment the matching block in the Privileges section of your
CLAUDE.md, so the agent knows it can run these commands directly.
The Limit of FTL for Access
Section titled “The Limit of FTL for Access”The credential cannot change configuration or import a Teleporter backup. FTL enforces both, unconditionally, with no setting to turn them off:
The current CLI session is not allowed to modify Pi-hole config settingsThat is the entire restriction.
Everything else the API offers stays available, which is why the caution above lists more than the four commands.
It’s still a stronger guarantee than an application password, where that same config restriction disappears the moment webserver.api.app_sudo is set to true.
Two practical limitations of FTL:
-
It assumes the default DNS port.
pihole allowlocates the API by asking FTL which port DNS runs on, and that lookup readspihole.toml, which the agent account deliberately cannot read. It falls back to the default of53, so a stock install works. If you changeddns.port, these commands fail for the agent and warn thatpihole.tomlcould not be read. Don’t solve that by granting access topihole.toml, which holds your password hashes. Run those commands yourself with!instead.
-
The grant survives restarts.
- FTL rewrites
cli_pwevery time it starts, but truncates the existing file rather than replacing it, so the ACL rides through exactly as the query database grant does. If you delete the file outright, re-run the grant above.
- FTL rewrites
To take this back, remove that one entry. Everything else on this page keeps working:
sudo setfacl -x "u:$USER" /etc/pihole/cli_pwsudo setfacl -x u:claude-agent /etc/pihole/cli_pwReplace pi-admin with your own account name on the Pi:
ssh -t pi-hole-admin 'sudo setfacl -x "u:pi-admin" /etc/pihole/cli_pw'ssh -t pi-hole-admin 'sudo setfacl -x u:claude-agent /etc/pihole/cli_pw'Optional: Run Diagnostics That Need Root
Section titled “Optional: Run Diagnostics That Need Root”A few things have no read-only path: pihole debug, systemctl status pihole-FTL, journalctl.
If you want the agent to run those unattended, grant a password-free rule for a wrapper script that takes no arguments.
Skip this section unless you need it. Nothing depends on it in later sections.
Grant a password-free rule for a wrapper script that takes no arguments.
-
Write a script that takes no input at all.
From the Pi sudo tee /usr/local/sbin/pihole-agent-diag >/dev/null <<"EOF"#!/bin/bashset -euo pipefailecho "=== FTL service ==="systemctl status pihole-FTL --no-pagerecho "=== last 100 FTL log lines ==="tail -n 100 /var/log/pihole/FTL.logEOFA heredoc doesn’t fit cleanly into a single quoted
sshremote command, so log in first:From your workstation ssh -t pi-hole-adminThen, on the Pi:
On the Pi, over that session sudo tee /usr/local/sbin/pihole-agent-diag >/dev/null <<"EOF"#!/bin/bashset -euo pipefailecho "=== FTL service ==="systemctl status pihole-FTL --no-pagerecho "=== last 100 FTL log lines ==="tail -n 100 /var/log/pihole/FTL.logEOF -
Make it root-owned and unwritable by anyone else.
If the agent can edit this script, a password-free rule on it is a root shell:
From the Pi sudo chown root:root /usr/local/sbin/pihole-agent-diag && \sudo chmod 755 /usr/local/sbin/pihole-agent-diagFrom your workstation ssh -t pi-hole-admin 'sudo chown root:root /usr/local/sbin/pihole-agent-diag && \sudo chmod 755 /usr/local/sbin/pihole-agent-diag' -
Allow exactly that script, with no arguments. The
""pins it to zero arguments:From the Pi echo "$USER ALL=(root) NOPASSWD: /usr/local/sbin/pihole-agent-diag \"\"" \| sudo tee /etc/sudoers.d/pihole-agent && \sudo chmod 440 /etc/sudoers.d/pihole-agent && \sudo visudo -c -f /etc/sudoers.d/pihole-agentFrom the Pi echo 'claude-agent ALL=(root) NOPASSWD: /usr/local/sbin/pihole-agent-diag ""' \| sudo tee /etc/sudoers.d/pihole-agent && \sudo chmod 440 /etc/sudoers.d/pihole-agent && \sudo visudo -c -f /etc/sudoers.d/pihole-agentReplace
pi-adminwith your own account name on the Pi:From your workstation ssh -t pi-hole-admin 'echo "pi-admin ALL=(root) NOPASSWD: /usr/local/sbin/pihole-agent-diag \"\"" \| sudo tee /etc/sudoers.d/pihole-agent && \sudo chmod 440 /etc/sudoers.d/pihole-agent && \sudo visudo -c -f /etc/sudoers.d/pihole-agent'From your workstation ssh -t pi-hole-admin 'echo "claude-agent ALL=(root) NOPASSWD: /usr/local/sbin/pihole-agent-diag \"\"" \| sudo tee /etc/sudoers.d/pihole-agent && \sudo chmod 440 /etc/sudoers.d/pihole-agent && \sudo visudo -c -f /etc/sudoers.d/pihole-agent' -
Confirm the pin holds. Run these separately, since the second is meant to be refused.
This should print the diagnostics:
From the Pi sudo -n /usr/local/sbin/pihole-agent-diagThe sudoers rule was granted to
claude-agent, so the check has to run as that account:From the Pi sudo -u claude-agent sudo -n /usr/local/sbin/pihole-agent-diagFrom your workstation ssh -t pi-hole-admin 'sudo -n /usr/local/sbin/pihole-agent-diag'From your workstation ssh pi-hole-agent 'sudo -n /usr/local/sbin/pihole-agent-diag'This should be rejected, because the rule allows no arguments:
From the Pi sudo -n /usr/local/sbin/pihole-agent-diag /etc/shadowFrom the Pi sudo -u claude-agent sudo -n /usr/local/sbin/pihole-agent-diag /etc/shadowFrom your workstation ssh -t pi-hole-admin 'sudo -n /usr/local/sbin/pihole-agent-diag /etc/shadow'From your workstation ssh pi-hole-agent 'sudo -n /usr/local/sbin/pihole-agent-diag /etc/shadow'
If you add this, uncomment the matching block in the Privileges section of your CLAUDE.md so the agent knows the wrapper exists.
Otherwise it will keep asking you to run diagnostics by hand.
Revoke Access
Section titled “Revoke Access”Remove the ACL entries.
The -R matters, because rotated archives inherited the grant from the directory’s default ACL:
sudo setfacl -R -x "u:$USER" /var/log/pihole && \ sudo setfacl -x "d:u:$USER" /var/log/pihole && \ sudo setfacl -x "u:$USER" /etc/pihole/pihole-FTL.db && \ sudo setfacl -x "u:$USER" /etc/pihole/cli_pw 2>/dev/null || trueThe cli_pw entry only exists if you granted it. The || true keeps the chain going when you didn’t.
Remove the sudoers rule if you added one:
sudo rm -f /etc/sudoers.d/pihole-agentDeleting the account removes its access everywhere at once:
sudo rm -f /etc/sudoers.d/pihole-agent /etc/sudoers.d/claude-agent-switch && \ sudo userdel -r claude-agentThen clear the now-orphaned ACL entries, which otherwise linger as a bare numeric UID:
sudo setfacl -R -b /var/log/pihole && \ sudo setfacl -b /etc/pihole/pihole-FTL.db && \ sudo setfacl -b /etc/pihole/cli_pwsetfacl -b clears every ACL entry on a file rather than removing one by name, so this succeeds whether or not you ever granted the cli_pw entry.
Remove the ACL entries. The -R matters, because rotated archives inherited the grant from the directory’s default ACL.
Replace pi-admin with your own account name on the Pi:
ssh -t pi-hole-admin 'sudo setfacl -R -x u:pi-admin /var/log/pihole && \ sudo setfacl -x d:u:pi-admin /var/log/pihole && \ sudo setfacl -x u:pi-admin /etc/pihole/pihole-FTL.db && \ sudo setfacl -x u:pi-admin /etc/pihole/cli_pw 2>/dev/null || true'The cli_pw entry only exists if you granted it. The || true keeps the chain going when you didn’t.
Remove the sudoers rule if you added one:
ssh -t pi-hole-admin 'sudo rm -f /etc/sudoers.d/pihole-agent'Deleting the account removes its access everywhere at once:
ssh -t pi-hole-admin 'sudo rm -f /etc/sudoers.d/pihole-agent /etc/sudoers.d/claude-agent-switch && \ sudo userdel -r claude-agent'Then clear the now-orphaned ACL entries, which otherwise linger as a bare numeric UID:
ssh -t pi-hole-admin 'sudo setfacl -R -b /var/log/pihole && \ sudo setfacl -b /etc/pihole/pihole-FTL.db && \ sudo setfacl -b /etc/pihole/cli_pw'setfacl -b clears every ACL entry on a file rather than removing one by name, so this succeeds whether or not you ever granted the cli_pw entry.
Confirm nothing is left.
No user: line should name the account you revoked:
getfacl -pR /var/log/pihole /etc/pihole/pihole-FTL.db /etc/pihole/cli_pw | grep '^user:'ssh pi-hole-admin "getfacl -pR /var/log/pihole /etc/pihole/pihole-FTL.db /etc/pihole/cli_pw | grep '^user:'"Alternative: The Pi-hole v6 API
Section titled “Alternative: The Pi-hole v6 API”Pi-hole v6 exposes query data over an HTTP API authenticated with an application password. It needs no filesystem changes at all, which makes it the right choice if:
- You administer Pi-hole but don’t have shell root on the Pi device itself, so you can’t set ACLs.
- Pi-hole runs in Docker (such as on TrueNAS), where the logs and database live inside the container and the ACL recipe above doesn’t apply as written.
-
Confirm which port the API is on.
Port
80is the default, but it’s marked optional, so FTL falls back silently, often to8080, when something else holds it:From the Pi pihole-FTL --config webserver.portFrom your workstation ssh pi-hole-admin 'pihole-FTL --config webserver.port'Use that port in the commands below. Every one of them targets
127.0.0.1, meaning Pi-hole’s own webserver, which only makes sense to run on the Pi itself - if you’re on a workstation, that’s exactly why each command below runs oversshrather than directly against the Pi’s address. -
In the web interface, go to Settings > Web interface / API, switch to Expert mode, and generate an application password.
-
Store it in a file only your user can read:
From the Pi mkdir -p ~/.config/pihole && \install -m 600 /dev/null ~/.config/pihole/agent-app-passwordFrom your workstation ssh pi-hole-admin 'mkdir -p ~/.config/pihole && \install -m 600 /dev/null ~/.config/pihole/agent-app-password'Paste the password in as a single line. Never put it in
CLAUDE.md, a dotfiles repo, or anything else you commit. -
Install
jq, which isn’t present by default:From the Pi sudo apt install jqFrom your workstation ssh -t pi-hole-admin 'sudo apt install jq' -
Authenticate and query. Piping the password through stdin keeps it out of the process list.
Replace
YOUR-DEVICE-IPwith the IP of a device on your network you want to check:From the Pi SID=$(jq -n --arg pw "$(cat ~/.config/pihole/agent-app-password)" '{password: $pw}' \| curl -s -X POST http://127.0.0.1/api/auth --data @- \| jq -r .session.sid) && \curl -s -H "X-FTL-SID: $SID" \'http://127.0.0.1/api/queries?client_ip=YOUR-DEVICE-IP&status=GRAVITY&length=50' | jqThis pipeline has too many quotes of its own to also wrap in a quoted
sshargument without a mistake, so log in first:From your workstation ssh -t pi-hole-adminThen, on the Pi, replacing
YOUR-DEVICE-IPthe same way:On the Pi, over that session SID=$(jq -n --arg pw "$(cat ~/.config/pihole/agent-app-password)" '{password: $pw}' \| curl -s -X POST http://127.0.0.1/api/auth --data @- \| jq -r .session.sid) && \curl -s -H "X-FTL-SID: $SID" \'http://127.0.0.1/api/queries?client_ip=YOUR-DEVICE-IP&status=GRAVITY&length=50' | jq
/api/queries filters server-side on client_ip, client_name, domain, status, upstream, type, reply, dnssec, from, and until.
Unlike the database, the API takes status names such as GRAVITY rather than the numeric codes.
Add disk=true to read the long-term database rather than the in-memory buffer.
On FTL 6.7 and later, sessions last 30 minutes. Each authenticated request extends the window.
Before the release of FTL 6.7, a logic bug (GHSA-w8cr-2cwg-92cg) kept the expiry check from running, so a session would periodically never expire. Update to 6.7 or later if you haven’t already.
Generate a new application password to revoke active sessions, including your web interface login.
What the Agent Can Still See
Section titled “What the Agent Can Still See”Read-only is not the same as private.
Any of the methods on this page expose every DNS query from every device on your network, including which sites each device reached and when. Least privilege limits what an agent can change, not what it can see.
If some of that shouldn’t leave the Pi, Pi-hole can filter it out of API results before anything reads them. Both settings take arrays of regular expressions, not plain strings, so anchor them and escape the dots:
sudo pihole-FTL --config webserver.api.excludeClients '["^192\\.168\\.0\\.50$"]' && \ sudo pihole-FTL --config webserver.api.excludeDomains '["^example\\.com$"]'The nested quotes here don’t survive being wrapped in another quoted ssh argument reliably, so log in first:
ssh -t pi-hole-adminThen, on the Pi:
sudo pihole-FTL --config webserver.api.excludeClients '["^192\\.168\\.0\\.50$"]' && \ sudo pihole-FTL --config webserver.api.excludeDomains '["^example\\.com$"]'Those filters reach only two API responses, /api/queries and /api/stats/top_clients.
They do nothing for an agent reading pihole-FTL.db through the ACL grant, which sees the unfiltered database.
If a device’s traffic genuinely shouldn’t be visible, the query log is the wrong place to fix that.