Monday, October 26, 2026

Kiro permissions.yaml: how it works

Kiro IDE 1.x controls what the agent can run with a permissions.yaml file. The matching rules are not obvious or well documented, so I tested the behavior on Windows with PowerShell. This post covers how matching works, what surprised me, and a short starter list of commands that are safe to allow.

Versions: most tests ran on Kiro 1.2.37 (October 5-7, 2026). On October 26, 2026 I re-ran a spot check of 12 commands on Kiro 1.4.26 (VS Code 1.131.0, Windows 10.0.26100). Every result matched. I did not re-run everything, so treat the details as observed behavior, not a spec.

Security philosophy

If you approach AI agent permissions with the perspective of a malicious agent hell-bent on breaking through, you will have to review every prompt, leading to user fatigue. There are too many bypasses.

Instead, I guard against serious errors, like an AI agent inferring "fix merge conflict" implies "and push to production." (That happened yesterday.)

If you need higher security, either review every prompt, run agents in a sandbox, or look into control through MCPs.

Where rules live

  • User level: ~/.kiro/settings/permissions.yaml (on Windows, %USERPROFILE%\.kiro\settings\permissions.yaml).
  • Workspace level: <repo>/.kiro/settings/permissions.yaml. I did not test this one.
  • Organization level: admins can deploy a managed policy that takes precedence. See the Kiro enterprise permissions docs.
  • Choosing "Always allow" in a prompt can write a rule to a separate file under ~/.kiro/workspace-roots/<id>/permissions.yaml. Review that file now and then, because nothing else shows you those rules.

Kiro reloads the file on the next tool call. No restart is needed.

I maintain a local, personal Git repository to manage my permissions and AGENTS.md file for multiple agents. It also includes documentation similar to this page. After an update, I launch a deploy script to copy it to the user profile path above.

File format

Each rule has a capability (shell, fs_read, fs_write, web_fetch, web_search), an effect (allow, ask, deny), and a list of patterns.

rules:
  - capability: shell
    effect: allow
    match:
      - git status *
      - git diff *

  - capability: shell
    effect: deny
    match:
      - git push -f *
      - git push *--force*

YAML quoting trap

A parse error makes Kiro deny every tool call, including the agent's own file edits, until you fix the file by hand. Inside double quotes, YAML treats a backslash as an escape, so "b:\sql" fails to parse. Worse, "C:\repo" loads silently because \r is a valid escape, and it becomes a rule that never matches. Use single quotes or no quotes for Windows paths.

How matching works

BehaviorExample
Rules match from the start of the command, word by word.git status * matches git status --short.
A trailing * also matches the bare command. You do not need both X and X *.nvidia-smi * matches plain nvidia-smi.
The space before * is a word boundary.Get-Item * does not match Get-ItemProperty.
A rule without * is exact.Write-Output does not match Write-Output 'hi'.
A * in the middle spans multiple words.snow.exe * --help matches snow.exe stage list --help.
The program name is literal. Each spelling is a different name.git, git.exe, and a full path to git need separate rules.
A flag placed before the subcommand breaks the match. This applies to deny rules too.git -C . status does not match git status *, and git -C . push -f escapes git push -f *.
Matching is case-sensitive, for the command name and for text inside arguments.GIT status and get-date do not match rules written in the usual case.
Quoting style and flag order are literal text.A pattern ending in " will not match single quotes or -q ... -c x order.
A command that starts with ( does not match a rule starting with the cmdlet name.(Get-Item x).Length prompts even with Get-Item * allowed. Use Get-Item x | Select-Object Length instead.

Compound commands

Kiro splits a command on ;, &&, and | and checks each part on its own.

  • Allow: every part must match an allow rule. If one part is unmatched, you get a prompt for the whole command.
  • Deny: if any part matches a deny rule, the whole command is blocked before anything runs. This holds wherever the denied part sits: first, last, after a pipe, or after cd. A deny also wins over an unmatched part, so you get a block and no prompt.
  • Subexpressions: code inside $(...) is checked, and so are recognized cmdlets inside Where-Object { } and & { } script blocks. A denied command inside is blocked, and an ask-listed command inside prompts.
  • Navigation: cd x; git status needs both cd * and a git rule.

Results from my tests, where "allowed" means both parts have allow rules:

CommandResult
nvidia-smi -L; where.exe git (both allowed)Ran, no prompt
nvidia-smi -L; hostname (second part unmatched)Prompt for the whole command
hostname; git push --force (unmatched + denied)Blocked, no prompt
git status --short && git push -f origin mainBlocked
git status --short $(git push -f origin main)Blocked

Precedence

  • A command with no matching rule prompts you.
  • Deny beats allow, and deny also beats Kiro's built-in allow list.
  • Kiro has a built-in auto-allow list that is separate from your file. It covers some read-only commands for plain git (status, log, show, blame, rev-parse). It did not cover git branch, git -C ..., or anything starting with git.exe. When you test rules, use a command that is not on that list, such as git.exe, or you cannot tell whether your rule or the built-in list allowed it.

Gotchas

The deny list is a speed bump, not a security control

  • Case: PowerShell is case-insensitive but Kiro's matching is not. FORMAT-VOLUME was not caught by deny rules for Format-Volume * or format-volume *. Add lowercase variants, knowing that mixed case still escapes.
  • Aliases: iex was not caught by a deny rule for Invoke-Expression *. I expect the same for other aliases but only tested this one.
  • Force pushes without -f: git push origin +main force-pushed past a rule set that blocked -f and --force. Remote branch deletion (--delete, -d, origin :branch) needs its own rules.
  • SQL keywords: a deny pattern like snow.exe sql * -q "*DROP*" missed lowercase, single quotes, --query, a different flag order, and the snow spelling without .exe. Broader patterns such as snow sql *DROP* catch more, and these blocked the lowercase and no-.exe forms on 1.4.26. Mixed case (Drop), -f file.sql, and piped input can still escape. The real control is a least-privilege database role.

The practical protection is that unknown commands still prompt, plus limits from the operating system itself.

Read commands with write flags

  • npm audit * includes npm audit fix. Deny npm audit fix *.
  • git log * and git diff * include --output=<file>, which writes a file. A deny rule on git log *--output* blocked it, even though git log * was allowed.
  • Redirection: an approved Get-Date > file.txt ran and wrote the file. Any broad allow rule for a cmdlet that produces output can overwrite files this way. I did not find a rule-based fix.
  • File readers such as cat * and Get-Content * read any file, including .env files and keys, and they bypass fs_read limits.

Be careful with * in allow rules

Because the middle * spans words, git -C * status * would also match git -C x push origin status. Keep write-capable commands out of broad allow patterns. In deny rules, a broad * is the safe direction. Over-blocking costs you a retry, not a mistake.

Deny messages are long

The automatic deny message lists every pattern in the matching rule block, not just the one that matched. For compound commands it ends with Triggered by: "<part>". If you want readable messages, split your deny rules into smaller blocks.

Starter allow list

These are read-only or low-risk, and common in agent workflows. Add the .exe spelling where your tools use it.

rules:
  - capability: shell
    effect: allow
    match:
      # Navigation, so "cd x; cmd" can pass when cmd is allowed
      - cd *
      - Set-Location *

      # Git, read-only (list both spellings)
      - git status *
      - git.exe status *
      - git diff *
      - git.exe diff *
      - git log *
      - git.exe log *
      - git show *
      - git.exe show *
      - git blame *
      - git.exe blame *
      - git rev-parse *
      - git.exe rev-parse *
      - git remote -v
      - git.exe remote -v
      - git branch --show-current
      - git.exe branch --show-current

      # PowerShell read-only cmdlets
      - Get-Command *
      - Get-Date *
      - Get-Item *
      - Get-ChildItem *
      - Test-Path *
      - Select-String *
      - Select-Object *
      - Where-Object *

      # Lookups
      - where.exe *

      # Python checks that do not run your code
      - python -m py_compile *
      - python --version

      # npm, read-only
      - npm ls *
      - npm audit *

A few choices behind that list:

  • I left out git -C ... on purpose. Run cd <repo>; git status instead, which works with the rules above.
  • I left out Get-Content and cat. They are popular, but they can read secrets. Add them if you accept that.
  • I left out npm test and python -m pytest. They run project code, so allow them only for repositories you trust.
  • Python is a program-name match like git. If the agent calls it by full path, you need a rule for that path (single-quoted in YAML).

Starter deny and ask list

Put these in separate blocks from your allow rules. Use ask for things you sometimes want, and deny for things you never want an agent to do.

  - capability: shell
    effect: ask
    match:
      - Remove-Item *
      - remove-item *
      - Clear-Content *
      - clear-content *
      - Stop-Process *
      - stop-process *

  - capability: shell
    effect: deny
    match:
      # Destructive git history operations (both spellings)
      - git push -f *
      - git push * -f
      - git push * -f *
      - git push *--force*
      - git push * +*
      - git push *--delete*
      - git push -d *
      - git push * -d *
      - git push * :*
      - git -C * push *
      - git filter-branch *
      - git filter-repo *
      - git reflog expire *
      - git.exe push -f *
      - git.exe push * -f
      - git.exe push * -f *
      - git.exe push *--force*
      - git.exe push * +*
      - git.exe push *--delete*
      - git.exe push -d *
      - git.exe push * -d *
      - git.exe push * :*
      - git.exe -C * push *
      - git.exe filter-branch *
      - git.exe filter-repo *
      - git.exe reflog expire *

      # Write flags on otherwise read-only commands
      - git log *--output*
      - git.exe log *--output*
      - git diff *--output*
      - git.exe diff *--output*
      - npm audit fix *

      # High-impact PowerShell (add lowercase copies; see case gotcha)
      - Invoke-Expression *
      - invoke-expression *
      - Start-Process *
      - start-process *
      - Set-ExecutionPolicy *
      - set-executionpolicy *
      - Format-Volume *
      - format-volume *
      - Restart-Computer *
      - restart-computer *

In my tests, the same ask rule for Remove-Item also prompted when the command sat inside a Where-Object { } or & { } block.

Checking your own rules

  • Test with commands that are harmless if they run: a nonexistent drive, -WhatIf, a nonexistent connection name, a repo with no remote.
  • Do not click "Always allow" or "Always deny" during tests. They can write rules into a permissions file behind your back.
  • Add a control for each test. For example, test a; b with an unmatched b first.
  • The agent only sees the tool result text. It can tell an automatic deny from a user rejection, but it cannot tell an auto-allowed run from one you approved by hand.

Steering

Combine permissions with steering. For example, it's not enough to deny git -C, I instruct agents to use an alternate form that works better with Kiro.

Summary

  • Patterns match from the start of the command, word by word, and are case-sensitive.
  • A trailing * covers the bare command and any arguments.
  • Compound commands are split on ;, &&, and |. Allow needs every part to match. One denied part blocks everything.
  • List each program spelling separately (git, git.exe).
  • Treat deny rules as a speed bump. Case, aliases, and unusual flags can get around them.
  • Watch for read commands with write flags, and for redirection.

These notes come from hands-on testing, not from Kiro's source or a complete specification. Behavior may change between versions.

Saturday, August 1, 2026

Python + GTK creates .dbus-keyrings on Windows

Why Does a Python + GTK Application Create .dbus-keyrings on Windows?

I noticed that running a Python + GTK on Windows application created :

%USERPROFILE%\.dbus-keyrings\org_gtk_gdbus_general

This application does not obviously use D-Bus, so I wanted to find out what was actually causing the file to appear.

Finding the trigger

Importing GTK or GIO alone does not create the file. Neither does Gtk.init_check(), or constructing a Gtk.Application.

Initially, entering the Gtk.Application event loop appeared to be the trigger. However, further experiments showed that this was only because GApplication.run() calls register().

The actual minimal reproducer is:

from gi.repository import Gio

app = Gio.Application(flags=Gio.ApplicationFlags.NON_UNIQUE)
app.register(None)

This creates the same 78-byte org_gtk_gdbus_general file, without GTK, an application ID, or a main loop.

I also confirmed that a plain Gio.Application behaves the same way as Gtk.Application. Using Gio.ApplicationFlags.NON_UNIQUE and omitting the application ID does not prevent the file from being created.

Conclusion

The file is a GDBus authentication keyring created as a side effect of GApplication.register(). The relevant path is essentially:

GApplication.register()
    → session D-Bus connection
    → GDBus authentication
    → org_gtk_gdbus_general keyring

So the application does not need to explicitly use D-Bus for this file to appear. Registering a GApplication is sufficient.

Test environment

  • PyGObject: 3.55.0
  • GTK: 3.24.51
  • Windows: 10.0.26220
  • Python: 3.12.9, 32-bit (MSC v.1944)

Sunday, February 8, 2026

Why "My Documents" Acts Weird in cmd.exe

On modern Windows, My Documents is not a normal folder. It is a legacy junction kept for old software. A junction is an NTFS reparse point that silently redirects one path to another, and Windows intentionally blocks listing it.

Initial behavior

C:\Users\handrew>dir /b Documents

No output because the folder was empty.

C:\Users\handrew>dir /b "My Documents"
File Not Found

This looks like the folder does not exist, but it actually does.

If a script insists on enumerating "My Documents", it will always think the folder is missing even though file reads and writes still work through the junction.

Creating files

C:\Users\handrew>type nul> "This is Documents">Documents\this_is_documents.txt

C:\Users\handrew>type nul> "This is My Documents">"My Documents"\this_is_my_documents.txt

Both commands succeed.

C:\Users\handrew>dir /b "My Documents"
File Not Found

Still cannot list it.

C:\Users\handrew>dir /b Documents
this_is_documents.txt
this_is_my_documents.txt

The file written through My Documents shows up in Documents.

Dir /al flag shows the junction

C:\Users\handrew>dir /al . | find "ocument"
<JUNCTION>     Local Settings [C:\Users\handrew\AppData\Local]

My Documents is a real NTFS junction, the kind Microsoft documents here: Hard Links and Junctions.

Trying to list from inside

C:\Users\handrew>cd "My Documents"

C:\Users\handrew\My Documents>dir
File Not Found

Listing is blocked on purpose.

Permissions explain everything

C:\Users\handrew>icacls "My Documents"
My Documents Everyone:(DENY)(RD)
             NT AUTHORITY\SYSTEM:(I)(OI)(CI)(F)
             BUILTIN\Administrators:(I)(OI)(CI)(F)
             WINDOWS11H2\handrew:(I)(OI)(CI)(F)

RD means Read Data or List Directory.

Windows allows path resolution and file creation, but blocks enumeration. This prevents infinite recursion and forces modern software to use the real Documents folder.

Explorer hides all of this. cmd.exe shows the cracks.

Bottom line: treat "Documents" as the real path, and expect legacy names like "My Documents" to resolve but not list. This was tested on Microsoft Windows 11 24H2 (OS Build 26100.7623).

Other junctions


C:\Users\handrew>dir /al

 Directory of C:\Users\handrew

11/09/2025  01:32 PM    <JUNCTION>     Application Data [C:\Users\handrew\AppData\Roaming]
11/09/2025  01:32 PM    <JUNCTION>     Cookies [C:\Users\handrew\AppData\Local\Microsoft\Windows\INetCookies]
11/09/2025  01:32 PM    <JUNCTION>     Local Settings [C:\Users\handrew\AppData\Local]
11/09/2025  01:32 PM    <JUNCTION>     My Documents [C:\Users\handrew\Documents]
11/09/2025  01:32 PM    <JUNCTION>     NetHood [C:\Users\handrew\AppData\Roaming\Microsoft\Windows\Network Shortcuts]
02/08/2026  01:54 PM    <DIR>          OneDrive
11/09/2025  01:32 PM    <JUNCTION>     PrintHood [C:\Users\handrew\AppData\Roaming\Microsoft\Windows\Printer Shortcuts]
11/09/2025  01:32 PM    <JUNCTION>     Recent [C:\Users\handrew\AppData\Roaming\Microsoft\Windows\Recent]
11/09/2025  01:32 PM    <JUNCTION>     SendTo [C:\Users\handrew\AppData\Roaming\Microsoft\Windows\SendTo]
11/09/2025  01:32 PM    <JUNCTION>     Start Menu [C:\Users\handrew\AppData\Roaming\Microsoft\Windows\Start Menu]
11/09/2025  01:32 PM    <JUNCTION>     Templates [C:\Users\handrew\AppData\Roaming\Microsoft\Windows\Templates]

C:\Users\handrew>dir /al c:\ProgramData

 Directory of c:\ProgramData

11/09/2025  11:30 AM    <JUNCTION>     Application Data [C:\ProgramData]
11/09/2025  11:30 AM    <JUNCTION>     Desktop [C:\Users\Public\Desktop]
11/09/2025  11:30 AM    <JUNCTION>     Documents [C:\Users\Public\Documents]
11/09/2025  11:30 AM    <JUNCTION>     Start Menu [C:\ProgramData\Microsoft\Windows\Start Menu]
11/09/2025  11:30 AM    <JUNCTION>     Templates [C:\ProgramData\Microsoft\Windows\Templates]

C:\Users\handrew>dir /al c:\users\public\documents

 Directory of c:\users\public\documents

11/09/2025  11:30 AM    <JUNCTION>     My Music [C:\Users\Public\Music]
11/09/2025  11:30 AM    <JUNCTION>     My Pictures [C:\Users\Public\Pictures]
11/09/2025  11:30 AM    <JUNCTION>     My Videos [C:\Users\Public\Videos]

Sunday, October 26, 2025

Over one hundred percentage memory usage on OpenWRT

OpenWrt Memory Mystery: When 796% Isn't Really 796%

Troubleshooting apparent memory overcommit on a Belkin RT3200 running OpenWrt 24.10.3

The Problem

I was checking my Belkin RT3200 router running OpenWrt 24.10.3, and both LuCI and the top command were showing something alarming: three processes were reporting a combined memory usage of nearly 800%.

Mem: 418740K used, 81752K free, 1528K shrd, 0K buff, 146184K cached
  PID  VSZ %VSZ %CPU COMMAND
 6562 1371m 281%   0% AdGuardHome
 3055 1287m 263%   0% tailscaled
12325 1231m 252%   0% cloudflared

On a router with only 500MB of RAM, how were three processes using nearly 4GB? The system was stable, running for 13 days without issues, and no swap was configured. Something didn't add up.

Understanding Virtual vs. Physical Memory

The key to this mystery lies in understanding the difference between virtual memory size (VSZ/VmSize) and resident set size (RSS/VmRSS).

Quick Definitions

  • VmSize (Virtual Memory Size): The total amount of virtual address space a process has reserved. This is not physical RAM.
  • VmRSS (Resident Set Size): The actual physical RAM currently being used by the process.

The Investigation

I dug deeper into /proc/[pid]/status for each process to see the real memory usage:

Process VmSize VmSize % VmRSS VmRSS % Ratio
AdGuardHome 1,404,572 kB 281% 121,284 kB 24.2% 11.6x
tailscaled 1,318,608 kB 264% 84,164 kB 16.8% 15.7x
cloudflared 1,261,440 kB 252% 32,524 kB 6.5% 38.8x
Total 3,984,620 kB 796% 237,972 kB 47.5% -
The revelation: While these three processes appeared to use 796% of available memory, they were actually only using 47.5% of physical RAM.

Why Go Programs Show High Virtual Memory

All three of these processes (AdGuardHome, Tailscale, and Cloudflared) are written in Go. The Go runtime has specific characteristics that lead to high virtual memory allocation:

  1. Aggressive address space reservation: Go's garbage collector and memory allocator reserve large chunks of virtual address space upfront for performance reasons.
  2. 64-bit address space abundance: On 64-bit systems (like this ARM64 router), virtual address space is essentially unlimited, so Go doesn't hesitate to reserve what it might need.
  3. Memory-mapped data structures: Go uses memory mapping for various internal structures, which shows up as virtual memory.
  4. Lazy allocation: Just because virtual memory is reserved doesn't mean physical pages are allocated. Linux only allocates physical RAM when memory is actually touched.

Why LuCI and Top Show VSZ

By default, OpenWrt's top command and LuCI show the VSZ metric because it's simpler to read from /proc and provides a quick overview. However, for Go programs, this metric is misleading.

The Better Commands

To see actual memory usage, use these commands:

# View RSS for all processes
ps aux | sort -k6,6n | tail -n 10

# Detailed memory breakdown for a specific process
cat /proc/[PID]/status | grep -E 'VmSize|VmRSS|RssAnon|RssFile'

# Quick snapshot of real memory usage
free -h

Should You Be Concerned?

In most cases, no. Here's when virtual memory size matters and when it doesn't:

Don't Worry If:

  • RSS (actual memory usage) is reasonable
  • The system is stable and responsive
  • You have plenty of available memory according to free -h
  • No OOM (Out of Memory) kills are occurring

Do Investigate If:

  • RSS is consuming most of your physical RAM
  • The system is swapping heavily (if swap is enabled)
  • You see OOM killer messages in dmesg
  • The system becomes unresponsive

System Configuration Notes

My router configuration that's relevant here:

  • Total RAM: 500MB (489 MiB)
  • Architecture: aarch64 (ARM Cortex-A53)
  • Overcommit setting: Mode 0 (heuristic)
  • Swap: None configured

The system uses vm.overcommit_memory = 0, which means the kernel uses heuristics to decide whether to allow memory allocations. This is perfect for this use case, as it allows Go programs to reserve virtual address space while preventing actual overcommit of physical memory.

Conclusions

What initially appeared to be a catastrophic memory problem turned out to be completely normal behavior for Go programs on Linux. The key lessons:

  1. Virtual memory ≠ Physical memory: VSZ shows address space reservations, not actual RAM usage.
  2. Look at RSS, not VSZ: For real memory consumption, always check RSS values.
  3. Go is verbose with address space: This is by design and not a problem on 64-bit systems.
  4. Monitor what matters: Focus on available memory, RSS usage, and system stability rather than VSZ percentages.

In my case, with 186MB of available memory and no OOM events over 13 days of uptime, the system is running perfectly fine despite the scary-looking 796% number.

Saturday, September 6, 2025

Benchmarking the TP-Link TL-WPA7510 Powerline kit

Real-World Powerline Networking Benchmarks

A practical look at HomePlug AV2 performance across outlets, with iperf3 & ping

TL;DR: Powerline can be acceptable—or terrible—depending on the outlet. In this home, one office outlet delivered ~86/51 Mbit/s (up/down) with low latency, while another office outlet managed only ~12/4 Mbit/s, far worse than Wi-Fi.

Testbed & Methods

These measurements were collected in a single residential home using a TP-Link TL-PA7010 (primary) + TL-WPA7510 (extender) HomePlug AV2 kit and a Netgear Nighthawk access point (AP)/switch. The primary adapter was Ethernet-wired to the AP; the client was usually a Dell Latitude laptop wired to the powerline extender. A Minisforum HM80 mini-PC on 5 GHz Wi-Fi served as the Wi-Fi control in the office, on another floor and across the house from the AP.

Note on hardware: My powerline kit is several years old. The TL-WPA7510 and TL-PA7010 are HomePlug AV2 adapters rated for up to 1000 Mbps (AV1000) and include a single Gigabit Ethernet port. The TL-WPA7510 also provides dual-band AC750 Wi-Fi (300 Mbps on 2.4 GHz and 433 Mbps on 5 GHz). These were mid-range when new, but they are not the fastest options available today. I rediscovered them after they were sitting in a drawer for five years, so I decided to run proper performance tests.

Metrics

  • iperf3 forward (client to router): upload capacity
  • iperf3 reverse (router to client): download capacity
  • Ping: min/avg/max RTT, loss

Commands

iperf3 -c 10.0.0.1        # forward (upload)
iperf3 -c 10.0.0.1 -R   # reverse (download)
ping 10.0.0.1 -n 100    # Windows, typical run

Note: iperf is LAN-LAN and reflects the link & LAN stack, not ISP bottlenecks. A wired control (Dell laptop wired to the AP) hit ~750-850 Mbit/s iperf with 1ms ping while Internet speedtests on that same link were ~175/36 Mbit/s, illustrating that the WAN pipe is lower than the LAN capabilities.

Results

Primary question: Should I switch my office from Wi-Fi to Powerline?

The main comparison is Wi-Fi 6 (5 GHz) vs Powerline, both in the office. We report medians to reduce run-to-run noise. These tests were in the same physical location, my office.

Scenario (Office) Client iperf3 forward (upload) iperf3 reverse (download) Ping avg
Wi-Fi control Minisforum HM80 35.85 Mbit/s 15.40 Mbit/s 5.45 ms
Powerline Dell laptop 11.60 Mbit/s 4.35 Mbit/s 9.0 ms
At this outlet, Powerline was ~68% lower on upload and ~72% lower on download than Wi-Fi, with slightly higher latency.

Outlet sensitivity: same room, different outcomes

Changing the office outlet dramatically changed Powerline results:

Powerline Outlet (Office) iperf3 forward (up) iperf3 reverse (down) Ping avg Verdict vs Wi-Fi
East wall (Test 6) 86.3 Mbit/s 51.3 Mbit/s ~3 ms Much faster than Wi-Fi
South wall (Test 7) ~29-35 Mbit/s ~16.8 Mbit/s ~5 ms Comparable to slightly worse
West wall (Test 9) ~9-12 Mbit/s ~4.2-4.5 Mbit/s ~9 ms Worse than Wi-Fi

What's possible near the base unit?

When the PL extender is near the primary PL adapter (same room), Powerline throughput was strong:

Scenario iperf3 forward (↑) iperf3 reverse (↓) Ping
Test 2 ~141-149 Mbit/s ~151-157 Mbit/s avg 2-3 ms
Test 3 ~137-146 Mbit/s ~165-166 Mbit/s avg ~3 ms

Wired control (for context)

The laptop wired directly to the Nighthawk achieved ~750-854 Mbit/s iperf, ping ~0-1 ms. Internet speedtests on that same link were ~175/36 Mbit/s due to the WAN cap. This underscores why iperf is the right tool for evaluating Powerline: it measures the LAN link itself, not your ISP.

Discussion: Why does outlet choice matter so much?

HomePlug AV2 uses wideband OFDM and, in its MIMO mode, can transmit across multiple wire pairs (Line-Neutral, Line-Ground, Neutral-Ground). This MIMO capability improves robustness and throughput, but it requires grounded 3-prong outlets to exploit those additional paths; 2-prong connections limit options and may reduce performance.

Also, the familiar "up to 1000 Mbps" rating is a physical-layer maximum under ideal conditions; real application throughput is much lower due to protocol overheads, electrical noise, and home wiring characteristics. Performance varies by outlet, distance, and circuit topology (breakers/phases/hops).

Modern Alternatives

Today's top powerline adapters are much faster and often include Wi-Fi 6. For example, the Devolo Magic 2 Wi-Fi 6 supports speeds up to 2,400 Mbps and offers mesh Wi-Fi with two Gigabit Ethernet ports. Another strong performer is the TP-Link TL-PA9020P KIT, which provides dual Gigabit ports and excellent wired performance for AV2 powerline. For those who need the absolute best wired speeds and have coaxial cabling, MoCA 2.5 adapters like the Actiontec ECB7250 or goCoax MoCA 2.5 deliver up to 2.5 Gbps over coax, far exceeding what powerline can achieve in most homes.

Powerline and MoCA are never plan A. If you can use wired Ethernet, it's the best option.

Key Findings

  • Outlet matters. In this home, Powerline ranged from sufficient(165/140 Mbit/s) to poor (12/4 Mbit/s) in the same room.
  • Under better conditions, powerline speeds were symmetrical, and under worse conditions download bandwidth was higher.
  • With the extender near the base, Powerline delivered ~140-165 Mbit/s. That is much lower than its nominal rating of 1000 Mbps, and Wi-Fi or Ethernet could do much better in the same room.
  • Versus Wi-Fi in the office: Powerline was either clearly better or clearly worse, depending on outlet.
  • Latency was consistently low (single-digit ms) on good Powerline links and competitive with Wi-Fi.

Recommendations

  1. Test multiple outlets in the target room; pick the one with the best iperf forward/reverse and low jitter.
  2. Use grounded, 3-prong wall outlets to enable AV2 MIMO paths (if your hardware supports it) and to reduce attenuation.
  3. Plug adapters directly into the wall; avoid surge protectors/filtered strips.
  4. If feasible, relocate the base unit to reduce breaker/phase "hops" to the destination outlet.
  5. Validate with iperf3 (both directions): run multiple trials and use median values.

Tuesday, August 19, 2025

Uninstall Metro bloatware from Samsung Galaxy Tab A9+

Cleaning T-Mobile and Metro Apps from a Tablet Using Ubuntu

This guide explains how to remove bloatware installed on a Metro tablet.

Metro apps send unwanted push notifications, install additional apps, and perform background telemetry and analytics. This guide will help you remove the visible App Hub and Metro Android App from your tablet, along with other background carrier apps, reducing unwanted notifications and telemetry while keeping your device fully functional. You do not need to root the tablet.

The guide is written for Ubuntu but is similar for other Linux distributions: you may need to replace the sudo apt with another package manager. Windows does not have apt or grep, but otherwise it is the same process.

Backstory: I bought a Galaxy Tab A9+ from Metro for free with a $20/month unlimited data plan. A voice plan was required, so I signed up for the $25 BYOD unlimited phone plan. The first $20 covered tax, fast shipping, activation, and everything else. A few months later, I downgraded the tablet to a $5/month 2GB plan. Others report that this plan can be canceled immediately after receiving the tablet. It's not a top-of-the line tablet, but I am happy with it. It's a little slow when switching apps, but otherwise it's what I was looking for.

Step 1: Enable Developer Mode on the Tablet

  1. Open Settings > About tablet.
  2. Tap Build number seven times to unlock Developer Options.
  3. Go back to Settings > Developer Options.
  4. Enable USB Debugging.

Step 2: Connect Tablet to Ubuntu

  1. Plug in your tablet via USB.
  2. Switch to the tablet and accept the Allow USB debugging prompt.

Step 3: Install ADB on Ubuntu

sudo apt update
sudo apt install adb

Step 4: Verify Connection

adb devices

You should see your tablet listed. If it says unauthorized like this, check the tablet and accept the prompt.

List of devices attached
A34B781CDF9	unauthorized

It should look like this (with a different device ID than mine):

List of devices attached
A34B781CDF9	device

Step 5: List T-Mobile and Metro Packages

This step is optional but recommended in case your package list differs:

adb shell pm list packages | grep -i tmobile
adb shell pm list packages | grep -i metro

Tip, if you see the error below, then look at the tablet for an authorization prompt:

adb: device unauthorized.
This adb server's $ADB_VENDOR_KEYS is not set
Try 'adb kill-server' if that seems wrong.
Otherwise check for a confirmation dialog on your device.

Step 6: Uninstall Packages for Your User

Run these commands to uninstall carrier apps without affecting system-wide functionality:

adb shell pm uninstall --user 0 com.applovin.array.apphub.tmobile
adb shell pm uninstall --user 0 com.dti.tmobile
adb shell pm uninstall --user 0 com.ironsrc.aura.tmo
adb shell pm uninstall --user 0 com.tmobile.dm.cm
adb shell pm uninstall --user 0 com.tmobile.dm.ms.services
adb shell pm uninstall --user 0 com.tmobile.pr.adapt
adb shell pm uninstall --user 0 com.metro.android.app
adb shell pm uninstall --user 0 com.metro.dm
adb shell pm uninstall --user 0 com.metro.services

For all packages except one, ADB responded with Success. However, the package com.tmobile.echolocate cannot be uninstalled via ADB. Attempting to remove it returns Failed to uninstall a package. It is a protected system service and will remain on the device.

Step 7: Verify Removal

adb shell pm list packages | grep -i tmobile
adb shell pm list packages | grep -i metro

Only protected or remaining apps will appear in the list.

Note: Some apps may return after Android upgrades unless you flash the tablet firmware. I am avoiding that for now because this adb process was quick and easy.

Monday, May 12, 2025

Estimating token counts from character length

When programatically using an AI chatbot API, it is easy to run up big bills. To avoid this, carefully moniter token usage, but resist the urge to estimate it by simply dividing character count. It may be inaccurate unless the following are true:

  • You use the same tokenizer. For example, Claude 3.5 Sonnet and Haiku 3.5 use the same tokenizer, but each vendor uses their own tokenizer.
  • Your inputs are always have the same proportion of text and images. If always text, that is a good start. If a short text plus image of same dimensions, that might work.
  • Your input prompts have low diversity. If some prompts are in English and others in Japanese, then the ratios will be wrong. Likewise, if some are mostly English while others are mostly source code, then your estimates will be poor.

Token counts for various documents

To make this more concrete, I tested two tokenizers across a variety of prompts:

Text type characters Amazon Nova Lite tokens Amazon characters/tokens ratio Claude 3 Haiku tokens Amazon vs Claude difference %
Python code 14,914 4,331 3.4 4,648 7%
news article English 6,555 6,034 1.1 6,104 1%
privacy policy in English and HTML 90,179 31,436 2.9 32,937 5%
JPEG photo, width 500px 74,660 970 77.0 503 -48%
Wikipedia article in Japanese 1,793 2,754 0.7 2,014 -27%
Different Wikipedia article in Chinese 2,715 5,717 0.5 4,662 -18%
Linux /var/log 8,543 3,942 2.2 3,503 -11%
minified javascript 20,856 9,294 2.2 9,366 1%
The Raven by Poe 6,838 1,628 4.2 1,900 17%
Russian poem in Cyrillic 591 277 2.1 274 -1%
SQL DML 9,265 5,843 1.6 4,851 -17%
Assembly code 10,621 4,265 2.5 4,842 14%
UTF-8 sampler 10,467 5,338 2.0 5,979 12%
Thai Wikipedia 7,645 6,223 1.2 6,592 6%
Bengali 7,191 6,119 1.2 6,624 8%
Microsoft policy in Korean 6,945 6,775 1.0 6,348 -6%
rare words, English 9,157 1,995 4.6 2,339 17%

Your results may vary, so run some tests. Tip: use your favorite API's "playground" function as a quick way to benchmark token counts without writing more code.

Correct code

Here is example code for accurately getting token counts from Amazon Bedrock.

    response = bedrock.converse(
        modelId=full_model_id,
        messages=conversation,
        inferenceConfig={"maxTokens": max_tokens, "temperature": temperature, "topP": top_p},)

    output_text = response["output"]["message"]["content"][0]["text"]
    output_characters = len(ret.output_text)
    input_tokens = response["usage"]["inputTokens"]
    output_tokens = response["usage"]["outputTokens"]
    

For OpenAI ChatGPT, check the usage field in the JSON structure.

Code for Google Gemini.

from google import genai

client = genai.Client()
prompt = "The quick brown fox jumps over the lazy dog."

# Count tokens using the new client method.
total_tokens = client.models.count_tokens(
    model="gemini-2.0-flash", contents=prompt
)
print("total_tokens: ", total_tokens)
# ( e.g., total_tokens: 10 )

response = client.models.generate_content(
    model="gemini-2.0-flash", contents=prompt
)

# The usage_metadata provides detailed token counts.
print(response.usage_metadata)
# ( e.g., prompt_token_count: 11, candidates_token_count: 73, total_token_count: 84 )

Kiro permissions.yaml: how it works

Kiro IDE 1.x controls what the agent can run with a permissions.yaml file. The matching rules are not obvious or well documented, so I test...