Skip to content

Add enforced read-only mode for repository analysis and code review #49

Closed

Description

@vicgupta

Summary

Add an opt-in, executor-enforced read-only mode for repository exploration, code review, and implementation planning. The model can use read_file, list_files, and grep; it cannot edit files or execute shell commands. Keep tinyagent's existing noninteractive, single-binary workflow and unrestricted default.

This is a feature proposal based on a verified capability gap and comparable workflows, not a claim of measured demand from tinyagent users. The interface names below are proposed for maintainer review.

Problem and opportunity

A task such as “review this package and suggest a migration plan” currently receives the same tools as “implement the migration.” Agent.Run always advertises all six tools, and Executor dispatches write/edit/bash calls without a run-specific permission check. A request or AGENTS.md instruction to avoid changes therefore depends on model compliance.

Developers and automation should be able to request analysis without granting model-controlled writes or command execution. This would add a distinct supported workflow, rather than merely changing a prompt. The existing file-reading, listing, and search tools already supply its core capabilities. Recent partial reads, filtered/case-insensitive search, and compaction make that workflow more practical without requiring shell access.

The proposal deliberately trades the ability to run tests, inspect Git through commands, and execute arbitrary analysis scripts for a small, enforceable tool set. It does not make untrusted repositories safe or prevent disclosure of readable source to the selected provider.

Research and evidence

Verified repository facts, inspected 2026-10-24 at c976760:

  • internal/agent/loop.go calls tools.Specs() for every fresh run and executes returned calls sequentially through a.Tools.Run. internal/tools/tools.go defines all six schemas and dispatches by tool name. Neither has a read-only policy.
  • The README's Notes explicitly document lexical path checks, following symlinks outside the root, and unsandboxed bash. bash.go runs bash -c; its working directory is not a security boundary.
  • Issue #32 identifies falling back to shell for filtered searches; merged PR #33 supplies native filtering. PR #13 adds partial reads and PR #45 adds case-insensitive search. These are concrete foundations for shell-free exploration, not evidence of a request for this mode.
  • Merged PR #28 adds a separate compacted model view while preserving audit history. Any tool restriction must remain in force across those additional requests.

External findings, checked 2026-10-24; documentation is living unless dated:

  • Aider's chat modes document an ask workflow for discussing code without changes, including a CLI mode selector. This establishes an analysis-before-editing workflow; it does not establish demand in tinyagent.
  • OpenCode's permissions documentation separates allow/ask/deny decisions and keeps explicit denials effective in automatic operation. The useful lesson here is execution policy independent of conversational instructions, without importing its full rule language.
  • Firsthand reports in OpenCode #16805, opened 2026-03-09, and #19094, opened 2026-03-25, describe writes through bash during planning. Both were later automatically closed for inactivity; closure is not verified remediation. These are historical reports from another project, not reproduced tinyagent failures or a statement about current OpenCode behavior. They motivate blocking the whole shell tool rather than trying to recognize “safe” commands.
  • OWASP's AI Agent Security guidance recommends task-specific tool sets and enforcing authorization outside model context. This supports checking both advertised capabilities and actual dispatch.

Interpretation: an explicit, small read-only mode fits tinyagent better than a general permissions framework for this initial feature. It provides a new analysis workflow with a bounded policy surface and no new service or tool integration.

Users and use cases

  • A developer asks for an architecture explanation or review of existing files before deciding whether to change them.
  • A script supplies a task on stdin and collects a textual review plus an optional trajectory, without authorizing shell commands or source edits.
  • A developer uses a local model for repository exploration while keeping the same CLI and tool restrictions as hosted providers.

Goals

  • Enable useful multi-step file inspection and analysis with only the three existing read tools.
  • Enforce the restriction independently of model behavior, task text, project instructions, streaming, and compaction.
  • Make the active mode visible and preserve existing unrestricted runs by default.

Non-goals

  • Interactive approvals, switching modes mid-run, arbitrary tool/path/command rule languages, or a new planning artifact format.
  • Running tests, builds, Git commands, interpreters, or supposedly read-only shell commands in this mode.
  • OS sandboxing, symlink confinement, secret filtering, network isolation, or a guarantee that no host file is written.
  • Resume, background processes, MCP integration, or changes to existing file-search semantics.

Proposed behavior

Proposed interface:

tinyagent --read-only --cwd ~/src/project -t "Review error handling in pkg/api; report findings"
cat review-task.md | tinyagent --read-only -t - -o review.json
  1. Add --read-only and optional YAML read_only: true. Omitted means false. An explicitly supplied boolean CLI value overrides the selected config, including --read-only=false; otherwise the config applies. Reuse current config discovery and strict unknown-key behavior. Only the operator's CLI/config selects the mode.
  2. Fix the mode for the whole invocation. In read-only mode advertise exactly read_file, list_files, and grep, with their existing arguments and limits. Explain their availability to the model without relying on that explanation for enforcement. Do not advertise write/edit/bash or newly added tools implicitly.
  3. At dispatch, permit only those three tools. A returned write_file, edit_file, bash, or any other nonallowed tool is rejected before its implementation or subprocess can run. Return an actionable tool error identifying the unavailable tool and active mode, using the original tool-call ID. Never request approval, retry it through another execution path, or silently enable it.
  4. For mixed batches, preserve existing sequential ordering: execute permitted calls, record one denial per prohibited call, and continue the conversation under the same policy. A denial is not successful task completion; existing submission/completion rules and run limits still govern termination. Do not invent a new exit-status taxonomy for this feature.
  5. Keep the mode effective for all subsequent requests, context-overflow retries, and after compaction. Task text, custom prompts, AGENTS.md, tool outputs, and summaries cannot grant permissions. If a requested task needs execution or edits, the model may explain the limitation; the harness still denies prohibited calls regardless of its answer.
  6. Emit a concise startup notice on stderr when read-only mode is active. Record the effective boolean in saved trajectory JSON as additive read_only metadata; omission on old artifacts is not evidence that a historical run was restricted. Preserve stdout's existing output conventions and all existing trajectory fields.
  7. Scope the promise to model tool authority. Explicit trajectory saving still writes its requested destination, provider requests still send context, and the existing Codex provider can still refresh its credential file. Existing AGENTS.md discovery, lexical path checks, and symlink behavior remain as documented. Help and README must explain these limits and that administrative actions such as --update are outside task-tool policy.

Acceptance criteria

  • With no new setting, existing tool schemas/order, dispatch, output, exit codes, and config behavior remain compatible; any trajectory addition is optional/additive.
  • Config-only, flag-only, omitted, and explicit true/false override cases select the documented mode. Invalid flag/config values fail before a model request. --help documents the mode without requiring task/config/credentials.
  • Every tool-enabled model request in read-only mode advertises exactly the three permitted tools, preserving their existing schemas. Tool-free compaction requests remain tool-free.
  • A scripted provider returning each prohibited tool, including a shell command that would write a marker, causes no corresponding write/edit/process execution. Denials retain call IDs and reach the next model request and saved trajectory. Unknown/future tool names are denied by default.
  • A mixed permitted/prohibited batch produces ordered results with no dropped or orphaned calls. A following permitted inspection succeeds. Existing step/time/cost limits and interruption behavior still terminate the run.
  • Tests force elision, summarization, and context-overflow retry, then return a prohibited call; each remains denied. Repository instructions and a custom system prompt requesting writes cannot alter dispatch policy.
  • A CLI run against a local fake provider completes a multi-step list/search/read review, produces the expected analysis output, and leaves fixture source contents/tree unchanged except for an explicitly requested trajectory destination. No shell executable is required for that workflow.
  • Provider-adapter tests verify filtered definitions and denied-call/result pairing for OpenAI-compatible, Anthropic, and Codex wire shapes, with streamed and nonstreamed paths where supported. No live credentials are required.
  • Active-mode notice and trajectory metadata agree. No-mode stdout/stderr behavior is preserved; policy diagnostics do not add argument dumps, file contents, or credentials.
  • Help/README document examples, precedence, blocked operations, output/auth writes, provider disclosure, and the existing filesystem limitations. Focused tests and the repository's format/build/vet/test checks pass on its declared toolchain before merge.

Technical context and constraints

Repository facts: go.mod declares Go 1.27.1 and YAML v3 as its direct external module. The Makefile builds static binaries for Linux/macOS/Windows on amd64/arm64. The public surfaces are CLI flags, YAML, trajectory JSON, and provider requests; Core implementation packages live under internal.

Relevant seams:

  • cmd/tinyagent/main.go: options, newFlagSet, config precedence via fs.Visit, executor construction, help, and stderr reporting.
  • internal/config/config.go: typed AgentConfig, defaults, and strict YAML decoding.
  • internal/tools/tools.go: shared tool schemas and central executor dispatch. Advertising fewer tools alone is insufficient; dispatch must enforce the same policy. The representation of that shared policy is an implementation choice.
  • internal/agent/loop.go and compact.go: initial schema selection, sequential tool results, protected model view, and retries.
  • internal/model/provider.go, openai.go, anthropic.go, codex.go: common tool/message contracts and adapter conversions; the feature should remain provider-independent.
  • internal/trajectory/trajectory.go: additive mode metadata. internal/model/codex.go already refreshes/saves credentials, explaining the host-write exception.

Proposed constraint: no new runtime service or dependency is needed for the core mode. Keep permitted schemas byte-stable within a run, including for Anthropic prompt caching. Review the proposed CLI/config/JSON names as public interfaces rather than treating a particular internal policy API as mandatory.

Alternatives considered

Major feature candidate Value, evidence, and fit Complexity, risk, maintenance, and decision
Enforced read-only analysis (selected) Verified unconditional tool authority; established analysis workflow and firsthand shell-bypass reports. Fits the existing executor and native read tools. Bounded cross-layer policy work; main risks are bypasses and overstating isolation. One fixed tool set keeps long-term policy maintenance smaller than general permissions.
Machine-readable live run events Existing automation inputs, trajectories, and OpenCode's JSON events support a plausible integration workflow. Requires a stable event schema and ordering/error/backpressure contract across streaming and tools. No specific tinyagent consumer requirement was found; less immediate evidence than the authority gap.
MCP external tools OpenCode documents local/remote MCP integration; tinyagent currently has only built-ins. Process/transport/auth/schema lifecycle, context overhead, and external-tool authority increase cost and risk. No concrete repository-specific integration need was found.
Checkpoint resume Strong fit with persisted histories and run limits. Already covered by open #42; rejected as duplicate. Its alternatives mention analysis permissions but explicitly exclude implementing them.
Background command sessions Useful server/watch workflows beyond synchronous bash. Already covered by open #41; rejected as duplicate, with separate process-lifecycle and buffering complexity.

Within the selected feature, prompt-only restrictions cannot enforce the outcome. Filtering shell command text adds bypass and portability risks. A general allow/ask/deny framework offers more flexibility but adds precedence, approval, and resource-matching semantics unnecessary for this fixed mode. The proposed differentiation is a small noninteractive contract, not novelty over larger agents.

Cross-cutting considerations

  • Security/privacy: use an explicit allowlist at execution, including future tool additions. Read-only is not confidentiality: readable secrets, outside-root symlink targets, project instructions, and saved transcripts retain existing exposure. Keep those limitations visible; do not advertise this as safe execution of hostile repositories.
  • Reliability/operations: denial is a recoverable tool result, not a process crash. Repeated denied calls remain subject to existing limits; unlimited settings retain their current meaning. No automatic escalation.
  • Performance: policy evaluation must not require another model request or scanning shell commands. Existing search/output/context limits still apply; this feature does not introduce a throughput claim.
  • Accessibility/supportability: plain-text help, mode notices, and denials must work without color, a TTY, or interactive input. Reuse trajectories for support rather than adding telemetry.
  • Compatibility/maintenance: default off; no data migration. Old binaries reject the new YAML key under strict decoding, so remove that key before downgrading. Review any future tool against the allowlist; shared policy should prevent schema/dispatch drift.

Test and verification expectations

Use the existing Go table tests, temporary fixture trees, scripted models, and httptest servers. Cover policy selection, schemas, dispatch bypass attempts, mixed calls, compaction/retry persistence, accounting/termination, adapter serialization, and unchanged default behavior. Assert source bytes/tree and subprocess nonexecution, rather than accepting a model's claim that it made no changes. Separate explicit host output writes from model-tool effects in fixtures.

Run the CI checks (gofmt -l ., go build ./..., go vet ./..., go test -count=1 ./...) and normal distribution cross-builds for compatibility. Include applicable Windows path/fixture handling; avoid requiring Unix shell availability for read-only-mode tests. These are implementation expectations; no feature implementation or test result is claimed here.

Documentation and rollout considerations

Ship as opt-in with help, YAML example, a review-from-stdin example, and a clear explanation of the three-tool boundary. Keep the complete vertical slice—selection, schema filtering, executor enforcement, feedback, and tests—in the initial delivery; a prompt-only intermediate version must not be presented as read-only mode. No service rollout, telemetry system, or database migration is required. Users can return to existing behavior explicitly; downgrades must remove the unsupported YAML key.

Dependencies and related work

  • #42 is related continuation work, not a prerequisite. Any future resume implementation must preserve or explicitly validate the operator's effective tool restrictions rather than silently escalating them.
  • #41 proposes process tools. If those land first, read-only mode must deny them by default.
  • #38 / open PR #48 propose literal search, which would improve native search usability but is not required for the mode.
  • PR #28 and the read/search PRs linked above are already-merged integration context, not new prerequisites. External workflow/security references are evidence, not runtime dependencies.

Duplication check: reviewed all current open/closed issue and PR titles/bodies, relevant comments and merged work, and searched read-only/readonly, permissions, approvals, sandbox, analysis/plan mode, and tool-policy terminology. No equivalent proposal was found. Discussions are disabled; no repository roadmap, design/RFC/ADR, CONTRIBUTING, AGENTS.md, or issue template was present in the inspected tree. #42 mentions this as an alternative, not a rejected maintainer decision or an included deliverable.

Open questions

  • Are --read-only, read_only in YAML, and additive read_only trajectory metadata the preferred public names? The required behavior is a fixed, explicit three-tool mode regardless of naming.
  • Should the startup notice enumerate the three tools, or name the mode and direct users to help? Either choice must identify the active restriction on stderr without changing stdout.

Activity

  1. added
    enhancementNew feature or request
    amf:readyReady for amf to turn into a pull request
    amf:failedamf could not process this issue; see its comment
    and removed
    amf:readyReady for amf to turn into a pull request
    on Oct 24, 2026
  2. vicgupta commented on Oct 24, 2026

    @vicgupta
    OwnerAuthor

    amf could not turn this issue into a pull request:

    opencode: exit status 1 (log: ~/amf-runner/tinyagent/.amf/logs/issue-49.log)
    
  3. added
    amf:readyReady for amf to turn into a pull request
    amf:doneamf opened a pull request for this issue
    and removed
    amf:failedamf could not process this issue; see its comment
    amf:readyReady for amf to turn into a pull request
    on Oct 24, 2026
  4. vicgupta commented on Oct 24, 2026

    @vicgupta
    OwnerAuthor

    amf opened #56 for this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    amf:doneamf opened a pull request for this issueenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions