Skip to content

Require the command path to open the command line - #17

Merged
dpleshakov merged 25 commits into
mainfrom
command-shape-for-agent-permissions
Oct 10, 2026
Merged

dpleshakov merged 25 commits into
mainfrom
command-shape-for-agent-permissions

Conversation

@dpleshakov

Copy link
Copy Markdown
Owner

Problem

AI agent hosts such as Claude Code and opencode allow or deny a shell command by matching its text, usually as a prefix (tfscli wit work-items get *). Cobra accepts flags anywhere, so tfscli --verbose wit work-items get 1 did not match a rule written for the command. A flag of an action placed before the action, as in tfscli wit wiql --time-precision query-by-wiql, also printed the group's help and exited 0 without running the query.

Approach

Before executing the tree, run locates the command and refuses the line with a config error unless the command path occupies the first words. The error shows the corrected command. The path is found by taking every word that names a subcommand, without reading flag values. An earlier version parsed flag values, but each divergence from pflag let a command run with its path in the wrong place. Not parsing flags is sound only if the tree keeps a fixed shape:

  • a group takes no arguments;
  • a group below the root defines no flags;
  • each command has exactly one name (no aliases, prefix matching, or case-insensitive matching).

Tests enforce this shape. An oracle test checks the rule against cobra over every command line of up to three words built from the tree's names and flags, and over a fixed-seed sample of longer lines.

The brief now records these command line rules. It also records the future gate flags for write operations (--allow-changes, --allow-irreversible), so that the rules hold once write commands are added.

Notes

  • This is a breaking change to the command line: a flag before the end of the path, --help included, is now refused. tfscli --help and tfscli --version without a path still work.
  • Cobra's hidden __complete command is the one exception. The words after it are the line being completed, not a command that runs.
  • The corrected command is shown only when every word can be quoted so that bash and PowerShell read it alike. Otherwise the message describes the reordering instead.
  • An unknown command under auth or completion is now an error instead of help with exit code 0.
  • The README documents the permission rules for Claude Code and opencode. They govern the usual form of a call and are not a security boundary.

dpleshakov and others added 25 commits October 8, 2026 09:33
AI agent hosts such as Claude Code and opencode decide whether an agent
may run a command by matching its text against allow, ask, and deny
rules. A prefix rule is reliable only if the command path always opens
the command line, and no rule can tell a writing command from a reading
one by its action name: action names come from the REST API and form an
open set, while Get Work Items Batch and Query By Wiql are POST requests
that only read.

The brief therefore requires flags to follow the command path, one form
per command, and local command names distinct from API areas. Future
write operations are gated by --allow-changes, and irreversible ones by
--allow-irreversible in addition, so that one ask rule covers every
change and one deny rule excludes the irreversible ones. The gate flags
have no environment variable or config key, since a setting outside the
command line is invisible to the rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
Cobra accepts flags at any position, so keeping the command path at the
start of the command line needs an explicit check in run, located with
root.Find. A root PersistentPreRunE was rejected: cobra runs only the
nearest such hook, and a child command defining its own would silently
drop the check. Flags cobra handles itself (--help, --version) and flags
the target command does not define are left to cobra.

The notes for future evolution record how writing commands will be
marked, so that the command tree test can require the gate flags of
exactly those commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
The rules are listed among the non-negotiable constraints so that a
future change is checked against them before it is made, with the full
wording left to the brief.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
An agent permission rule such as "tfscli wit work-items get *" matches
the command text by its prefix, so "tfscli --verbose wit work-items get
1" slipped past it. run now locates the target command with root.Find
and refuses a flag token before the last word of its path with a config
error that shows the corrected command, before any request is sent.

The check yields to cobra wherever it cannot place an argument: an
unknown command, a flag the target command does not define (--help and
--version included, since cobra adds them only on execution), the "--"
terminator, and a stray positional argument. Moving an unknown flag would
not make the command valid, so cobra's own error is the better one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
Agent permission rules match the command text, so a second name for a
command would let it run under a form no rule was written for. The
rule is recorded in the brief; this test makes make check enforce it
for every command in the tree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
The README states that flags follow the command and gives ready rules
for Claude Code and opencode that allow the reading commands and deny
auth, which an agent has no reason to run. The opencode rules say only
what its documentation does: the last matching rule wins. The agent
skill tells the agent to keep flags after the command rather than rely
on the error, and the changelog records the refusal as a change to the
command line.

Closes docs/archive/2026-10-08-tasks-command-shape-permissions.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
cobra's Find decides whether a flag takes a value only from the flags of
the command it stands on. A flag unknown there swallows the next word:
--help before cobra registers it, or a flag of an action placed at the
level of its group. "tfscli --help wit work-items get" failed with an
unknown command, and "tfscli wit wiql --time-precision query-by-wiql"
printed the help of wiql with exit 0 without sending the query, a false
success the flag order check let through because Find stopped at the
group.

The check now walks the arguments from the root and decides whether a
flag takes a value by a flag of that name on the current command or
anywhere below it, so it always reaches the real command. Every flag
before the end of the path is refused, --help included; --version, which
only the root defines, gets a message of its own. help and completion
are registered when the tree is built, so they are walked like any other
command rather than exempted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
With the command path found by walking the arguments, --help follows the
path like any other flag, and help and completion are ordinary
commands, so the brief, the README, and the agent skill no longer list
exceptions; only an invocation without a path, such as tfscli --help,
has none. The architecture describes the walk and why cobra's Find is
not used. The changelog records the --help case and, as a fix, the
false success of a flag placed before its action.

Closes docs/archive/2026-10-08-tasks-command-path-walk.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCREyMqPZyHxuCRoy5FZqf
The walk added in the previous change is a second parser of the command
line, and it diverged from cobra's in edge cases a review found. "tfscli
wit work-items --fields -p get 5" passed the check and sent the request:
the walk took the value -p for a new flag and stopped at work-items. It
also stopped at words cobra skips ("" and "-") and at a word cobra takes
as the value of a flag it does not know ("--help X"), while cobra went on
to run the command behind them.

The walk now reads a pending value before anything else, as pflag does,
and the check locates the command both by the walk and by Find and holds
to the deeper of the two: each catches the other stopping early. The
command passes only when its path occupies exactly the first words.

The message no longer lists the misplaced flags, since a stray word is
not a flag and an attached short value made the list inaccurate; the
corrected command shows what moves. A flag the command does not define
is reported as unknown in pflag's wording, rather than moved into a
command that would fail the same way. The corrected command is quoted
for the shell in single quotes, which bash and PowerShell read alike.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
The README said a call through tfscli.exe or a full path is asked about,
which holds for Claude Code but not for opencode, where a command
matching no rule is allowed unless a catch-all ask is set; it now says
so, and that the deny rule guards the usual form of the call rather than
being a security boundary. Its examples gain an ask rule on
--allow-changes, since future write commands will fall under the same
"tfscli wit *" allow rule as the reading ones.

CONTRIBUTING repeats the brief's constraints for contributors who do not
open it, and now lists the command line rules and the gate flags as
CLAUDE.md does. The architecture describes the cross-check with Find,
unknown flags, and the quoting of the corrected command.

Closes docs/archive/2026-10-09-tasks-flag-order-review.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
The check located the command twice, by a walk that repeated pflag's
rules for flag values and by cobra's Find, and took the deeper result.
Both defects found so far were divergences between that walk and pflag.

The shape of the command tree makes flag values irrelevant: a command
with subcommands only groups them, taking no argument and defining no
flag of its own, so in a valid command line no word naming a subcommand
can follow the path. The command is now the one reached by taking, in
order, every word that names a subcommand of the command reached so far.
That reaches at least as deep as Find, which descends only through such
words, so the walk over flag values and the cross-check are gone; every
case of the existing tests passes unchanged.

The rule now depends on the shape of the tree, so the command tree test
requires it of every group. It found at once that auth was built as a
plain cobra command rather than with newGroupCmd, and that "tfscli auth
bogus" printed the help with exit 0; auth is now built like the other
groups and reports an unknown command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
The flag order check now relies on the shape of the command tree, so
the shape becomes a rule of the brief, repeated in CLAUDE.md and
CONTRIBUTING: a command with subcommands takes no arguments, defines no
flags of its own, and only prints its help. A group that also acts on
its own, as git stash does, is excluded, since a rule written for the
group would cover every command below it. The architecture describes the
check without the walk over flag values and records why that walk was
rejected. The changelog records the fix to unknown commands under auth.

Closes docs/archive/2026-10-09-tasks-simple-path-rule.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
Cobra adds its hidden __complete command only when it executes the tree,
so the check took "tfscli __complete wit ''" for a misplaced path and
broke shell completion. Such a request runs no command.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
A test runs the check and cobra over every command line of up to three
words built from the names and flags of the tree, and over a fixed-seed
sample of longer lines: a line the check accepts must make cobra select
a command whose path opens the line, and the corrected command proposed
for a refused line must itself be accepted and select the command the
message names.

The test found three divergences, now fixed:

- "--" no longer stops the search for the path, since pflag takes it for
  the value of a flag before it, as in "tfscli --api-version -- completion";
- a flag before "__complete" is refused like any other, the completion
  request being taken for a subcommand of the root that ends the path;
- the path is moved to the front until it stays there, so that the
  correction of "tfscli - login auth" is "tfscli auth login -" rather
  than a line refused again.

Closes docs/archive/2026-10-09-tasks-flag-order-oracle.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
- Read the words before the path against the final path: when the path
  was found in more than one pass, flags before it went unchecked, and
  "tfscli work-items wit --bogus get 5" was told to move --bogus after
  the path instead of being told it is unknown.
- Read "--" before the path as pflag does, as the end of the flags
  rather than as an unknown flag named "--"; report "--=x" and "---x"
  as bad flag syntax; read a group of shorthands letter by letter; and
  refuse --version only where pflag would read it as a flag.
- Refuse cobra's case-insensitive command matching in the tree test.
- Extend the oracle test to refusals for a flag before the path, give
  each worker one tree, keep completion requests from writing cobra's
  diagnostics to stderr, and report failures in a stable order; turn off
  cobra's Explorer check for the whole package in TestMain.
- Narrow the group rule in CLAUDE.md and CONTRIBUTING.md to areas,
  resources, and local groups; describe the quoting, the reading of the
  words before the path, and the oracle in docs/architecture.md.
- Merge the changelog entry on --time-precision into the entry on the
  flag order; in README, put the opencode deny rule last and state that
  the rules govern the usual form of a call, not a determined agent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XA4Hx4kZtgobXtNCEpuiTC
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVURGFoSzFiQP2uJD8fLu7
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVURGFoSzFiQP2uJD8fLu7
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVURGFoSzFiQP2uJD8fLu7
…tion check (TASK-04)

The wider oracle alphabet showed that cobra stops at the first flag value it
cannot parse, which the check does not read, so the oracle now accepts that
outcome instead of expecting a later flag to be reported.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVURGFoSzFiQP2uJD8fLu7
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XVURGFoSzFiQP2uJD8fLu7
The ask rule on --allow-changes matched any command carrying the string,
so it also prompted for unrelated commands such as a search or a commit
message that mentions the flag; it now starts with tfscli like the other
rules.

The section states the rules and the precedence they need independently
of an agent, and keeps the Claude Code and opencode settings as examples
whose format belongs to the agent. The section is unwrapped to match the
rest of the file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmjswYSvuZ7yb3erKTWJH3
The list repeats the brief to catch a violation without opening it, so
it names each rule and leaves the detail to the brief. The command line
rule keeps every part but drops the parenthetical expansions, which tests
in internal/cli already enforce in part. The gate flags shrink to a
clause of the read-only item: no write operation is added without a
product decision, which means reading the brief anyway. The note on the
gate flags in the configuration precedence item repeated that item and
is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmjswYSvuZ7yb3erKTWJH3
The cli entry in architecture.md retold the flag order check line by
line: the quoting for bash and PowerShell, the repeated move of the
path, cobra's completion command, pflag's wording, the details of the
oracle test. The comments in internal/cli/flagorder.go already say all
of it next to the code, so the entry keeps what the code does not: the
check and why it needs no flag values, the tests that guard the tree
shape, and the rejected alternatives. Both data flows now start with
the check.

The brief keeps the product decisions and drops cobra internals: the
hidden completion command becomes one sentence on shell completion, and
the OPTIONS detail of the gate, already in the architecture, goes. The
gate flags are said to stand outside the configuration chain once, in
"Configuration".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmjswYSvuZ7yb3erKTWJH3
The tasks skill described itself as the workflow for any work that starts
now, so an agent asked to fix the findings of a branch review planned the
fixes as a tasks file. This branch gained five such files on top of the one
for the feature itself, each with a two-task plan that was never agreed
with anyone, and TASK-01 and TASK-02 now refer to several files in one
branch's history.

A tasks file is now reserved for work that needs a plan: several tasks, a
breakdown agreed with the user before the work starts, or work that may
outlast one session. Review fixes, single fixes, and small requested changes
are done directly and explained in their commit messages, even when the user
leaves the details to the agent. A fix that turns into a change of approach
gets a tasks file once the user has approved the decision. The rule is in
the skill rather than in CONTRIBUTING.md because it does not depend on the
project; the global copy of the skill carries the same text.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015c62YtYEvUtpS8VvRmvRb4
CLAUDE.md is loaded into every agent session, so it names each rule and
leaves the detail to the brief, as CONTRIBUTING does. The gate flags
shrink to a clause of the read-only item, and the command line rule
drops the expansions that the command tree tests enforce. The gate flags
are no longer mentioned in the configuration precedence item, and the
command shape section no longer repeats that flags follow the action.

That no flag gives a reading command a side effect is a rule of the
command line, in force before any write operation exists, and was lost
from CONTRIBUTING together with the gate details; both files now state
it with the other command line rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HmjswYSvuZ7yb3erKTWJH3
@dpleshakov
dpleshakov marked this pull request as ready for review October 10, 2026 06:30
@dpleshakov
dpleshakov merged commit 537b7d7 into main Oct 10, 2026
4 checks passed
@dpleshakov
dpleshakov deleted the command-shape-for-agent-permissions branch October 10, 2026 06:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant