Enhance security checks and improve I18N handling#160
Enhance security checks and improve I18N handling#160alimuzzaman wants to merge 22 commits intomasterfrom
Conversation
- Add CLAUDE.md with project overview, architecture notes, and security summary - Add .ai/ agent context directory (security vulnerability docs excluded from git) - Exclude .ai/, .ai/security/, and CLAUDE.md from distribution via .distignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Referer-based AJAX context check with capability-based check. Sub-site admins could forge the Referer header to gain network-admin privileges in AJAX handlers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sub-site admins could POST is_network_admin=1 to apply network-wide config changes without Super Admin privileges. Add current_user_can() check after nonce verification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…settings() #2 Sub-site admins could supply arbitrary blog_ids to permanently delete comments from any site in the network. Add current_user_can() check and per-blog membership validation before switch_to_blog(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Malicious role names containing HTML could execute in the admin session when selected for exclusion. Fix PHP source (esc_html on translate_user_role) and JS sink (encode labels before .html()). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…enumeration #5 Any authenticated user could enumerate all subsite names, IDs, and comment-disable status across the network via the AJAX endpoint. Restrict to users with manage_network_plugins or manage_options. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix #4 regression: WordPress is_network_admin() always returns false during AJAX, so the capability checks in disable_comments_settings() and delete_comments_settings() always fell back to manage_options, allowing site admins to still trigger network-wide operations. Also restores network-wide avatar management which was silently broken by the same regression (line 1269 branch never reached). Fix: read formArray before the capability check, derive context from the trusted-and-sanitized is_network_admin POST flag, and gate the avatar network loop on both the flag and manage_network_plugins. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Improve I18N Issue based on 2.5.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR tightens security around multisite/network actions in the Disable Comments plugin and improves I18N + output escaping to reduce XSS risk, while adding AI/developer documentation and updating release ignore rules.
Changes:
- Added capability checks to AJAX handlers for settings save, comment deletion, and sub-site listing; tightened multisite deletion to sites a user belongs to.
- Improved escaping of role labels and dynamic UI strings in PHP/JS; adjusted a few strings for proper translation handling.
- Added developer/agent docs (
CLAUDE.md,.ai/README.md) and updated.distignore/.gitignoreto exclude agent artifacts from releases.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| disable-comments.php | Network permission checks, output escaping/I18N tweaks, multisite deletion restrictions, and network-admin detection change |
| assets/js/disable-comments-settings-scripts.js | Escapes role labels before injecting into HTML strings |
| CLAUDE.md | Adds plugin architecture/dev guide for contributors/agents |
| .gitignore | Ignores Claude worktrees and .ai/security notes |
| .distignore | Excludes AI/agent files and adds pnpm lockfile to dist ignore |
| .ai/README.md | Documents purpose/structure of the .ai/ context directory |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
disable-comments.php
Outdated
| wp_send_json(['data' => [], 'totalNumber' => 0]); | ||
| } | ||
|
|
||
| if (!current_user_can('manage_network_plugins') && !current_user_can('manage_options')) { | ||
| wp_send_json(['data' => [], 'totalNumber' => 0]); |
There was a problem hiding this comment.
get_sub_sites() currently allows either manage_network_plugins OR manage_options. In multisite setups where site admins can access the plugin settings page (e.g., when site-wide settings are enabled), this would let non-network admins enumerate all site IDs and blog names via AJAX. Consider restricting this endpoint to network admins/capability (manage_network_plugins) when is_multisite() (or otherwise gating it so it only returns data for authorized network-level users), and return a proper JSON error/HTTP status for unauthorized requests instead of an empty dataset.
| wp_send_json(['data' => [], 'totalNumber' => 0]); | |
| } | |
| if (!current_user_can('manage_network_plugins') && !current_user_can('manage_options')) { | |
| wp_send_json(['data' => [], 'totalNumber' => 0]); | |
| wp_send_json_error( | |
| [ | |
| 'message' => __('Invalid request. Please refresh the page and try again.', 'disable-comments'), | |
| 'data' => [], | |
| 'totalNumber' => 0, | |
| ], | |
| 400 | |
| ); | |
| } | |
| // Restrict access more strictly on multisite: only network admins may enumerate subsites. | |
| if (is_multisite()) { | |
| if (!current_user_can('manage_network_plugins')) { | |
| wp_send_json_error( | |
| [ | |
| 'message' => __('Sorry, you are not allowed to access this resource.', 'disable-comments'), | |
| 'data' => [], | |
| 'totalNumber' => 0, | |
| ], | |
| 403 | |
| ); | |
| } | |
| } else { | |
| if (!current_user_can('manage_options')) { | |
| wp_send_json_error( | |
| [ | |
| 'message' => __('Sorry, you are not allowed to access this resource.', 'disable-comments'), | |
| 'data' => [], | |
| 'totalNumber' => 0, | |
| ], | |
| 403 | |
| ); | |
| } |
disable-comments.php
Outdated
| $is_network_action = !empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1'; | ||
| $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; | ||
| if (!$this->is_CLI && !current_user_can($required_cap)) { | ||
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); | ||
| } |
There was a problem hiding this comment.
New capability gating was added to the AJAX settings handlers, but there are no unit/integration tests covering the “network action requires manage_network_plugins vs. site action requires manage_options” behavior. Given this is security-sensitive logic, consider adding PHPUnit coverage to ensure unauthorized users receive an error and authorized users can still save settings.
disable-comments.php
Outdated
| return true; | ||
| } | ||
| return false; | ||
| return is_network_admin(); |
There was a problem hiding this comment.
is_network_admin() previously treated multisite AJAX requests originating from Network Admin as “network” by checking the request referer, which ensured network-initiated AJAX calls loaded/saved site options correctly. After simplifying to return is_network_admin();, network admin AJAX requests routed through admin-ajax.php will likely report false, which can cause the constructor to load per-site options during network actions (e.g., affecting $old_options usage when saving network settings). Consider restoring an AJAX-safe network-context detection (e.g., checking for network admin AJAX endpoint or a verified server-side flag) or explicitly loading site options inside the network action handlers when is_network_action is true.
| return is_network_admin(); | |
| // First, rely on WordPress' native network admin detection. | |
| if (is_network_admin()) { | |
| return true; | |
| } | |
| // For AJAX requests routed through admin-ajax.php, is_network_admin() | |
| // will often return false even when the request originated from the | |
| // Network Admin. In that case, fall back to checking the referer. | |
| if (defined('DOING_AJAX') && DOING_AJAX) { | |
| $referer = wp_get_referer(); | |
| if ($referer && false !== strpos($referer, network_admin_url())) { | |
| return true; | |
| } | |
| } | |
| return false; |
disable-comments.php
Outdated
| // translators: %s: disabled post types. | ||
| echo '<div class="notice notice-warning"><p>' . sprintf(esc_html__('Note: The <em>Disable Comments</em> plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(esc_html__(', ', 'disable-comments'), $names_escaped)) . '</p></div>'; | ||
| echo '<div class="notice notice-warning"><p>' . sprintf(__('Note: The <em>Disable Comments</em> plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(__(', ', 'disable-comments'), $names_escaped)) . '</p></div>'; |
There was a problem hiding this comment.
discussion_notice() now uses __() and echoes the translated string directly while allowing embedded HTML (<em>). Since this output isn’t passed through wp_kses_post() (or otherwise escaped), a compromised/hostile translation could inject unwanted markup. Consider keeping the translation string plain text (move emphasis tags outside the translatable string) and escape with esc_html__(), or wrap the final formatted string in wp_kses_post() while ensuring dynamic pieces remain escaped.
- is_network_admin() now detects network context in AJAX via ?is_network_admin=1 URL param, but only when current user has manage_network_plugins — eliminates spoofing concern and fixes constructor loading wrong options during AJAX - JS passes is_network_admin=1 in URL for all three AJAX calls when on network admin - Both handlers now derive $is_network_action from $this->is_network_admin() for AJAX (single authoritative source) and fall back to $formArray for CLI - disable_comments_settings() explicitly loads get_site_option() for $old_options on network actions, preventing disabled_sites corruption via AJAX - get_sub_sites() now requires manage_network_plugins on multisite (vs OR logic that allowed any site admin to enumerate all subsites) - Removes redundant current_user_can() re-check on avatar loop Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
assets/js/disable-comments-settings-scripts.js:321
disable_comments_save_settingscan now returnwp_send_json_error(...)for permission failures, which will yieldresponse.success === falsein thissuccesshandler when the server responds 200. Right now the UI never resets the button / shows an error in that case, leaving it stuck in the “Saving…” state. Add handling for!response.success(and/or a.failhandler if you switch to 403 status codes) to restore the button and display the error message.
jQuery.ajax({
url: networkAjaxUrl,
type: "post",
data: data,
beforeSend: function () {
var btnText = __("Saving Settings..", "disable-comments");
saveBtn.html(
'<svg id="eael-spinner" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 48 48"><circle cx="24" cy="4" r="4" fill="#fff"/><circle cx="12.19" cy="7.86" r="3.7" fill="#fffbf2"/><circle cx="5.02" cy="17.68" r="3.4" fill="#fef7e4"/><circle cx="5.02" cy="30.32" r="3.1" fill="#fef3d7"/><circle cx="12.19" cy="40.14" r="2.8" fill="#feefc9"/><circle cx="24" cy="44" r="2.5" fill="#feebbc"/><circle cx="35.81" cy="40.14" r="2.2" fill="#fde7af"/><circle cx="42.98" cy="30.32" r="1.9" fill="#fde3a1"/><circle cx="42.98" cy="17.68" r="1.6" fill="#fddf94"/><circle cx="35.81" cy="7.86" r="1.3" fill="#fcdb86"/></svg><span>' + btnText + '</span>'
);
},
success: function (response) {
if (response.success) {
saveBtn.html(__("Save Settings", "disable-comments"));
Swal.fire({
icon: "success",
title: response.data.message,
timer: 3000,
showConfirmButton: false,
});
saveBtn.removeClass('form-dirty').prop('disabled', true);
savedData = $form.serialize();
}
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
disable-comments.php
Outdated
| : $this->is_network_admin(); | ||
| $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; | ||
| if (!$this->is_CLI && !current_user_can($required_cap)) { | ||
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); |
There was a problem hiding this comment.
wp_send_json_error() here doesn’t set an HTTP status code. Since this error is an authorization failure, returning a 403 status (and ensuring the JS handles non-2xx responses appropriately) makes the behavior consistent with get_sub_sites() and prevents UI states getting stuck on an error response.
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); | |
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); |
disable-comments.php
Outdated
| if (!$this->is_CLI) { | ||
| $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; | ||
| if (!current_user_can($required_cap)) { | ||
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); |
There was a problem hiding this comment.
Same as above: this permission failure returns JSON without an HTTP 403 status. Using a 403 (and handling it client-side) improves correctness and aligns with the other AJAX handler (get_sub_sites) which already returns a 403 on insufficient permissions.
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')]); | |
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); |
disable-comments.php
Outdated
| 'disabled_post_type_count' => array( | ||
| 'label' => __('Disabled Post Types Count', 'disable-comments'), | ||
| 'value' => sprintf('%d of %d', count($data['disabled_post_types']), $data['total_post_types']), | ||
| 'value' => sprintf(__('%1$d of %2$d', 'disable-comments'), count($data['disabled_post_types']), $data['total_post_types']), |
There was a problem hiding this comment.
This uses __() inside sprintf() for a value intended to be plain text in Site Health. Using an escaping i18n helper (e.g., esc_html__()) prevents translations from injecting markup into the debug output.
| 'value' => sprintf(__('%1$d of %2$d', 'disable-comments'), count($data['disabled_post_types']), $data['total_post_types']), | |
| 'value' => sprintf(esc_html__('%1$d of %2$d', 'disable-comments'), count($data['disabled_post_types']), $data['total_post_types']), |
| var saveBtn = jQuery("#disableCommentSaveSettings button.button.button__success"); | ||
| var deleteBtn = jQuery("#deleteCommentSettings button.button.button__delete"); | ||
| var savedData; | ||
| var networkAjaxUrl = ajaxurl + (disableCommentsObj.is_network_admin === '1' ? '?is_network_admin=1' : ''); |
There was a problem hiding this comment.
networkAjaxUrl is built by string-concatenating '?is_network_admin=1'. If ajaxurl ever already contains a query string, this will generate an invalid URL. Consider using a small helper to append the parameter with ?/& correctly (or build via URL/URLSearchParams).
| var networkAjaxUrl = ajaxurl + (disableCommentsObj.is_network_admin === '1' ? '?is_network_admin=1' : ''); | |
| var appendQueryParam = function (url, key, value) { | |
| var separator = url.indexOf('?') === -1 ? '?' : '&'; | |
| return url + separator + encodeURIComponent(key) + '=' + encodeURIComponent(value); | |
| }; | |
| var networkAjaxUrl = disableCommentsObj.is_network_admin === '1' | |
| ? appendQueryParam(ajaxurl, 'is_network_admin', '1') | |
| : ajaxurl; |
disable-comments.php
Outdated
| return !empty($_GET['is_network_admin']) | ||
| && $_GET['is_network_admin'] === '1' |
There was a problem hiding this comment.
is_network_admin() reads $_GET['is_network_admin'] directly in an AJAX context. In WP admin requests, superglobals may be slashed and should be accessed via wp_unslash() + sanitization (or strict validation) to avoid PHPCS violations and inconsistent behavior. Consider reading from $_REQUEST, unslashing/sanitizing, and then doing the '1' comparison.
| return !empty($_GET['is_network_admin']) | |
| && $_GET['is_network_admin'] === '1' | |
| $is_network_admin = ''; | |
| if (isset($_REQUEST['is_network_admin'])) { | |
| $is_network_admin = wp_unslash($_REQUEST['is_network_admin']); | |
| if (!is_string($is_network_admin)) { | |
| $is_network_admin = ''; | |
| } | |
| $is_network_admin = sanitize_text_field($is_network_admin); | |
| } | |
| return ('1' === $is_network_admin) |
disable-comments.php
Outdated
| // translators: %s: disabled post types. | ||
| echo '<div class="notice notice-warning"><p>' . sprintf(esc_html__('Note: The <em>Disable Comments</em> plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(esc_html__(', ', 'disable-comments'), $names_escaped)) . '</p></div>'; | ||
| echo '<div class="notice notice-warning"><p>' . sprintf(__('Note: The <em>Disable Comments</em> plugin is currently active, and comments are completely disabled on: %s. Many of the settings below will not be applicable for those post types.', 'disable-comments'), implode(__(', ', 'disable-comments'), $names_escaped)) . '</p></div>'; |
There was a problem hiding this comment.
This notice now outputs a raw translated string containing HTML via __() and echoes it without any escaping/sanitization. To avoid translator-introduced XSS while still allowing intended markup (e.g., <em>), sanitize the final HTML with wp_kses_post() (or wp_kses() with an explicit allowlist) and keep dynamic pieces escaped as they already are.
- Add HTTP 403 status to wp_send_json_error() permission failures in both AJAX handlers for consistency with get_sub_sites() - Sanitize $_REQUEST['is_network_admin'] with wp_unslash/sanitize_text_field per WordPress coding standards - Fix networkAjaxUrl to use ?/& correctly if ajaxurl already has query params - Use esc_html__() for plain-text Site Health value (no markup needed) - Wrap discussion_notice() output in wp_kses_post() to guard against translator-injected markup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_sub_sites() nonce failure now returns proper 403 error instead of
HTTP 200 with empty success-shaped data
- Remove esc_html() on role names before JSON encoding — caused double-
encoding since JS already escapes via $('<span>').text()
- Save button: add else branch to reset button and show error when
response.success is false with HTTP 200
- Delete button: add .fail() handler so button resets on HTTP error
(403, network failure) instead of staying stuck in Deleting state
- Swal delete messages: use text: instead of html: to prevent XSS
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_sub_sites: add formatAjaxError to pagination args so nonce/cap failures show a Swal error instead of silently rendering empty list - Save error handler: extract server message from jqXHR.responseJSON, fix hardcoded button label to use __(), fix old Swal type: -> icon: - Delete fail handler: extract server message from jqXHR.responseJSON Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
disable-comments.php:1183
get_sub_sites()computes$required_capfor non-multisite installs, but it still callsget_sites()unconditionally. Since the handler is registered on all installs, calling this AJAX action on a single-site setup can trigger a fatal error ifget_sites()isn’t available. Add an earlyis_multisite()/function_exists('get_sites')guard and return a JSON error when multisite APIs aren’t present.
$required_cap = is_multisite() ? 'manage_network_plugins' : 'manage_options';
if (!current_user_can($required_cap)) {
wp_send_json_error(['message' => __('Sorry, you are not allowed to access this resource.', 'disable-comments')], 403);
}
$_sub_sites = [];
$type = isset($_GET['type']) ? sanitize_text_field(wp_unslash($_GET['type'])) : 'disabled';
$search = isset($_GET['search']) ? sanitize_text_field(wp_unslash($_GET['search'])) : '';
$pageSize = isset($_GET['pageSize']) ? sanitize_text_field(wp_unslash($_GET['pageSize'])) : 50;
$pageNumber = isset($_GET['pageNumber']) ? sanitize_text_field(wp_unslash($_GET['pageNumber'])) : 1;
$offset = ($pageNumber - 1) * $pageSize;
$sub_sites = get_sites([
'number' => $pageSize,
'offset' => $offset,
'search' => $search,
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $nonce = (isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : ''); | ||
|
|
||
| if (($this->is_CLI && !empty($_args)) || wp_verify_nonce($nonce, 'disable_comments_save_settings')) { | ||
| $formArray = $this->get_form_array_escaped($_args); | ||
| $is_network_action = $this->is_CLI | ||
| ? (!empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1') | ||
| : $this->is_network_admin(); | ||
|
|
||
| if (!$this->is_CLI) { | ||
| $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; | ||
| if (!current_user_can($required_cap)) { | ||
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); | ||
| } | ||
| } |
There was a problem hiding this comment.
delete_comments_settings() also returns wp_send_json_success() even when the nonce check fails, because the success response is sent unconditionally after the guarded block. This can report a successful deletion (often with a meaningless “.” message) for invalid/expired nonces. Send a JSON error (HTTP 403) and exit when nonce verification fails.
| $is_network_action = $this->is_CLI | ||
| ? (!empty($formArray['is_network_admin']) && $formArray['is_network_admin'] == '1') | ||
| : $this->is_network_admin(); | ||
|
|
||
| if (!$this->is_CLI) { | ||
| $required_cap = $is_network_action ? 'manage_network_plugins' : 'manage_options'; | ||
| if (!current_user_can($required_cap)) { | ||
| wp_send_json_error(['message' => __('Insufficient permissions.', 'disable-comments')], 403); | ||
| } | ||
| } | ||
|
|
||
| if (!empty($formArray['is_network_admin']) && function_exists('get_sites') && class_exists('WP_Site_Query')) { | ||
| if ($is_network_action && function_exists('get_sites') && class_exists('WP_Site_Query')) { | ||
| $sites = get_sites([ | ||
| 'number' => 0, | ||
| 'fields' => 'ids', | ||
| ]); | ||
| foreach ($sites as $blog_id) { | ||
| // $formArray['disabled_sites'] ids don't include "site_" prefix. | ||
| if (!empty($formArray['disabled_sites']) && !empty($formArray['disabled_sites']["site_$blog_id"])) { | ||
| if (!is_super_admin() && !is_user_member_of_blog(get_current_user_id(), $blog_id)) { | ||
| continue; // Skip sites the user doesn't belong to | ||
| } |
There was a problem hiding this comment.
The new permission logic and multisite membership filtering in delete_comments_settings() isn’t covered by tests. Add PHPUnit tests to assert 403s for insufficient capability and nonce failure, plus a multisite test where a non-super-admin cannot delete comments on a site they’re not a member of (ensuring those sites are skipped/blocked as intended).
…ngs() Previously, a failed nonce check would fall through to wp_send_json_success(), reporting a successful deletion with a "." message. Nonce is now verified as an early-exit guard that sends a 403 error and dies immediately. Also added wp_die() after the capability check error response for consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@copilot review again. But skip tests for now. |
…ings() Agent-Logs-Url: https://github.com/WPDevelopers/disable-comments/sessions/8293ceb0-586f-466d-a783-948b52cc6525 Co-authored-by: alimuzzaman <6744156+alimuzzaman@users.noreply.github.com>
Reviewed all actionable non-test comments. Most were already addressed in prior commits. The one remaining issue found: |
disable_comments_settings()nonce failure silently returning success — nonce check is now an early-exit guard (matchingdelete_comments_settings()pattern), returning HTTP 403 instead of falling through towp_send_json_success()get_sub_sites()— already restricted tomanage_network_pluginson multisite with HTTP 403discussion_notice()— already useswp_kses_post()to sanitize translated HTMLdisable_comments_settings()permission failure — already returns HTTP 403delete_comments_settings()nonce + permission failures — already return HTTP 403 with early exitsprintf— already usesesc_html__()networkAjaxUrl— already safely appends query param usingindexOf('?')checkis_network_admin()superglobal access — already uses$_REQUEST+wp_unslash()+sanitize_text_field()delete_comments_settings()✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.