TUI can offer permission grants without displaying the requested permission rule
What version of Codex CLI is running?
0.144.6
What subscription do you have?
Plus
Which model were you using?
_No response_
What platform is your computer?
MacOS
What terminal emulator and version are you using (if applicable)?
zsh
Codex doctor report
What issue are you seeing?
On two occasions during normal Codex CLI use, the TUI asked me whether I wanted to grant additional permissions, but it did not display the permissions being requested.
I did not capture the exact wording, but the prompt was approximately:
Would you like to grant these permissions?
[the requested permission rule was missing here]
1. Yes, ...
2. Yes, ...
3. No, ...
The important detail is that this was a permission approval prompt, not a command-execution or file-change approval prompt.
There may have been a Reason: line, but there was no visible Permission rule: line identifying the filesystem paths, network access, or other permission scope. This meant I could not determine what access I was being asked to grant.
I saw this twice, but I did not capture the serialized request or a screenshot. Therefore, I am not claiming that I have identified the exact upstream producer responsible for those two occurrences.
What steps can reproduce the bug?
Reproduction status
There are two different reproduction levels.
1. Real-world observation
I observed the blank permissions prompt twice during ordinary Codex CLI use.
This occurrence was intermittent, and I did not capture the exact request payload. Therefore, the exact upstream trigger is not yet confirmed.
2. Deterministic TUI-level reproduction
The renderer-level condition is deterministic:
- Construct or deliver a
PermissionsApprovalRequestwhoseRequestPermissionProfilehas no displayable rule. format_requested_permissions_rule()returnsNone.build_header()omits thePermission rule:line.build_options()still creates affirmative permission-grant choices.- The resulting dialog asks the user to grant permissions without showing which permissions will be granted.
A regression test can construct that request directly and assert that an invalid permissions request does not expose any affirmative grant option.
Example test intent:
#[test]
fn empty_permissions_request_does_not_offer_grant_options() {
let request = permissions_request_with_no_displayable_rule();
let overlay = ApprovalOverlay::new(/* request and test dependencies */);
let rendered = render_overlay(&overlay);
assert!(rendered.contains(
"permission scope could not be displayed"
));
assert!(!rendered.contains(
"Yes, grant these permissions"
));
}
The exact test helper names can follow the existing codex-tui test conventions.
What is the expected behavior?
A permissions approval prompt should always display a concrete and inspectable permission rule before offering any affirmative grant option.
For example:
Would you like to grant these permissions?
Reason: The command needs to update Git metadata.
Permission rule: write `/workspace/project/.git`
1. Yes, grant for this turn
2. Yes, grant for this session
3. No, continue without permissions
If Codex receives an empty, malformed, unsupported, or otherwise non-displayable permission profile, the TUI should fail closed and must not offer a grant option.
For example:
Codex requested additional permissions, but the permission scope could not be displayed.
For safety, this request cannot be approved.
1. Deny and continue
Actual behavior
The current approval overlay builds the permissions title and grant choices independently of whether the requested permission profile can be formatted for display.
In codex-rs/tui/src/bottom_pane/approval_overlay.rs, the permissions header adds a Permission rule: line only when:
format_requested_permissions_rule(&request.permissions)
returns Some(...).
Conceptually, the current flow is:
if let Some(rule_line) =
format_requested_permissions_rule(&request.permissions)
{
header.push(/* Permission rule */);
}
// The normal grant choices are still created even when rule_line is None.
format_requested_permissions_rule() can return None when the permission profile contains no displayable network or filesystem rule. However, build_options() still constructs the normal permissions title and affirmative choices:
Would you like to grant these permissions?
This means the renderer does not currently enforce the invariant:
A permission request must have a visible permission subject before the user can grant it.
Additional information
Relationship to #16283
This report is closely related to:
That issue reported a permissions prompt with no Permission rule: line because filesystem permissions were lost during a lossy JSON conversion in the TUI app-server path. It also caused the approval response to contain an empty permission profile.
The current code now uses typed conversion:
permissions: params.permissions.try_into()?,
which addresses that specific conversion failure.
However, the current renderer still permits an empty or non-displayable permission profile to reach the approval overlay and still offers grant choices. Therefore, the remaining problem is broader:
Even if one previously known producer-side data-loss bug was fixed, the approval UI still does not fail closed when it cannot display the requested permission scope.
If my two observations occurred on a version predating the typed-conversion fix, they may be the same root cause as #16283. If they occurred on a version containing that fix, this may be a recurrence through another producer path or a separate empty-profile path.
Other approval paths with the same missing-subject invariant
The permissions prompt is the issue I actually observed. The following paths are included as supporting evidence that approval-subject validation should be centralized. I am not claiming that these paths caused my two observed prompts.
Command execution
In the TUI app-server conversion, a missing command is converted to an empty command vector:
command: params
.command
.as_deref()
.map(split_command_string)
.unwrap_or_default(),
The overlay can still build:
Would you like to run the following command?
and affirmative choices.
A commandless approval can be valid when a visible structured network target is present. The invalid case is when there is neither a command nor another visible approval subject.
File changes
The TUI conversion currently constructs file-change requests with:
changes: HashMap::new(),
and the file-change overlay builds:
Would you like to make the following edits?
with affirmative choices. The file-change header does not currently render the changes field.
This resembles the symptom reported in:
MCP elicitation
MCP approval uses the server name and message as its visible subject. The overlay does not appear to enforce that the message is non-empty before offering approval choices.
This is related to, but not a duplicate of:
Proposed fix
I think the fix should have two layers.
1. Validate each request before offering affirmative options
Introduce a shared check that determines whether an approval request has a concrete, visible subject.
Conceptually:
fn has_displayable_approval_subject(
request: &ApprovalRequest,
) -> bool {
match request {
ApprovalRequest::Permissions(request) => {
format_requested_permissions_rule(
&request.permissions,
)
.is_some()
}
ApprovalRequest::Exec(request) => {
request.network_approval_context.is_some()
|| !request.command.is_empty()
|| request
.additional_permissions
.as_ref()
.and_then(
format_additional_permissions_rule,
)
.is_some()
}
ApprovalRequest::ApplyPatch(request) => {
!request.changes.is_empty()
}
ApprovalRequest::McpElicitation(request) => {
!request.message.trim().is_empty()
}
}
}
The precise conditions may need adjustment to match all intended approval variants, but the invariant should be:
No affirmative approval option is available unless the operation or permission scope is concretely displayed.
2. Fail closed in the overlay
Before constructing normal approval choices:
if !has_displayable_approval_subject(request) {
return invalid_approval_options(/* deny/cancel only */);
}
The invalid state should:
- explain that the requested action or permission scope could not be displayed;
- log enough structured diagnostic information to identify the producer;
- offer only deny or cancel;
- never silently treat a generic
Reason:as the permission subject.
A reason explains why access is requested. The permission rule explains what access will be granted. The former must not substitute for the latter.
Suggested tests
Permission-specific tests
permissions_request_with_empty_profile_does_not_offer_grantpermissions_request_displays_filesystem_rulepermissions_request_displays_network_rulepermissions_request_typed_conversion_preserves_filesystem_entriespermissions_grant_response_preserves_the_displayed_profile
Shared approval-invariant tests
empty_exec_without_network_context_does_not_offer_approvalcommandless_network_approval_with_visible_host_remains_validempty_file_change_request_does_not_offer_approvalempty_mcp_message_does_not_offer_approvalevery_affirmative_approval_has_a_visible_subject
Diagnostic improvement
Because this can be intermittent, it would also help to emit a structured warning when an approval request has no displayable subject.
For example:
approval request rejected: no displayable subject
kind=permissions
thread_id=<redacted>
turn_id=<redacted>
item_id=<redacted>
The log should avoid exposing sensitive paths or command contents unless the existing logging policy permits them.
This would make it possible to distinguish:
- an empty profile created by a producer;
- a failed protocol conversion;
- an unsupported permission variant;
- and a renderer-formatting defect.
Why I believe this should be handled in the TUI as well as upstream
Upstream validation and typed protocol conversion are important, but the approval UI is the final security boundary before a user authorizes an operation.
Even if all known producers are corrected, a future regression, version mismatch, malformed request, or unsupported permission type could again result in a non-displayable subject.
A final fail-closed renderer guard prevents the UI from ever asking the user to approve an unidentified operation.
Related issues
Closest permissions report. Its specific root cause was lossy permission conversion. This report focuses on the remaining fail-closed rendering invariant.
Related blank or uninformative file-change approval.
Related approval-clarity problem in the MCP elicitation path, but not the same permissions request path.
Contribution
Per the current contribution policy, I have not opened a pull request:
I have prepared the renderer-level reproduction, proposed regression-test cases, and a possible fail-closed design.
If this issue is suitable for external contribution and the proposed direction aligns with the maintainers' intended solution, I would be happy to prepare and submit an invited, focused PR.
2 Comments
I’ve created a minimal reproducible example.
Start codex cli with
--enable request_permissions_tooland--ask-for-approval on-request.The prompt would be:
i traced this against current
origin/main(fe01054a28) and the remaining TUI boundary is still there:build_header()can omit thePermission rule:line whenformat_requested_permissions_rule()returnsNone, whilebuild_options()still offers the normal grant choices.current core
request_permissionsnow rejects thenetwork.enabled=falserepro as empty after normalization, so that producer path is covered. the overlay can still fail open if a malformed / version-skewed permissions event reaches it.i pushed a focused fork branch here, no upstream PR because
docs/contributing.mdsays unsolicited PRs are closed:https://github.com/erichanwang/codex/tree/fix/permissions-approval-deny-only
patch shape:
Codex requested additional permissions, but the permission scope could not be displayed.No, continue without permissionsvalidation:
cargo fmt --manifest-path codex-rs/Cargo.toml --package codex-tui --checkpassedcargo check --manifest-path codex-rs/Cargo.toml -p codex-tui --libpassedorigin/mainbefore this patch:tui/src/app/tests.rs:4414passesServerRequesttoAppServerEvent::ServerRequest, whose variant now expectsBox<ServerRequest>.